pediatric-ai-scribe-v3/test/resource-sharing.test.js
Daniel c7864f763e
Some checks failed
Forgejo Docker Build / Root app tests (push) Successful in 56s
Forgejo Docker Build / Build Docker image (push) Successful in 10s
Forgejo Docker Build / End-to-end (browser) (push) Failing after 12s
feat: sharing is by link — follow it, accept, and it is in your resources
The author presses "Copy a share link" and sends it however they like.
Whoever follows it (app.pedshub.com/#share=<token>) is signed in first if
need be — the token survives the trip through the SSO — then shown what it
is and who from, and adds it with one press. Only the token's hash is
stored; a link lasts 30 days and can be withdrawn; accepting twice is
harmless; the owner following their own link changes nothing. Sharing by
email is gone: nobody is looked up by address. "Everyone signed in" stays
as a switch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 15:20:35 +02:00

51 lines
4.2 KiB
JavaScript

// Sharing a resource: reading is extended to named people or to everyone
// signed in; writing never is.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const read = f => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
test('every read route goes through the reader rule; every write route still filters on the owner', () => {
const route = read('src/routes/myResources.js');
assert.match(route, /WHERE r\.id = \? AND \(r\.user_id = \? OR r\.shared_with_all OR s\.user_id IS NOT NULL\)/);
for (const marker of ["router.get('/my-resources/:id'", "router.get('/my-resources/:id/export'", "router.get('/my-resources/:id/preview'", "router.get('/my-resources/:id/preview/:page'"]) {
const body = route.slice(route.indexOf(marker), route.indexOf('\n});', route.indexOf(marker)));
assert.match(body, /await readableResource\(req\.params\.id, req\.user\.id/, marker + ' reads through the rule');
}
for (const marker of ["router.put('/my-resources/:id/theme'", "router.post('/my-resources/:id/refine'", "router.delete('/my-resources/:id'", "router.put('/my-resources/:id'"]) {
const body = route.slice(route.indexOf(marker), route.indexOf('\n});', route.indexOf(marker)));
assert.match(body, /AND user_id = \?/, marker + ' stays the owner\'s');
assert.doesNotMatch(body, /readableResource/, marker + ' is not opened to readers');
}
// The share list itself is the owner's, and adding by email never lists accounts.
for (const marker of ["router.get('/my-resources/:id/shares'", "router.put('/my-resources/:id/shares/all'", "router.post('/my-resources/:id/share-link'", "router.delete('/my-resources/:id/shares/:userId'"]) {
const body = route.slice(route.indexOf(marker), route.indexOf('\n});', route.indexOf(marker)));
assert.match(body, /await ownedResource\(req\.params\.id, req\.user\.id\)/, marker + ' is owner-only');
}
// A link: only its hash is stored, it can expire and be withdrawn, and
// accepting it adds the resource for the caller — never for someone named.
assert.match(route, /crypto\.randomBytes\(24\)\.toString\('base64url'\)/);
assert.match(route, /INSERT INTO user_resource_share_links \(resource_id, token_hash, created_by, expires_at\)/);
assert.match(route, /WHERE l\.token_hash = \? AND l\.revoked_at IS NULL AND \(l\.expires_at IS NULL OR l\.expires_at > NOW\(\)\)/);
const accept = route.slice(route.indexOf("router.post('/my-resources/share-link/:token/accept'"), route.indexOf('\n});', route.indexOf("router.post('/my-resources/share-link/:token/accept'")));
assert.match(accept, /\[link\.resource_id, req\.user\.id, link\.owner_id\]/, 'the caller is the one added');
assert.match(accept, /ON CONFLICT DO NOTHING/, 'accepting twice is not an error');
assert.match(accept, /if \(link\.owner_id !== req\.user\.id\)/, 'the owner following their own link changes nothing');
assert.doesNotMatch(route, /WHERE email = \?/, 'nobody is shared with by address; accounts are never looked up by email');
});
test('the list carries whose each row is, and the page offers changes only on your own', () => {
const route = read('src/routes/myResources.js');
assert.match(route, /\(r\.user_id = \?\) AS owned, r\.shared_with_all/);
assert.match(route, /CASE WHEN r\.user_id = \? THEN NULL ELSE u\.name END AS shared_by_name/);
const js = read('public/js/myResources.js');
assert.match(js, /if \(row\.owned === false\) \{ return wrap; \}/, 'a shared row has Preview and downloads, nothing else');
assert.match(js, /library\.filter\(function \(row\) \{ return row\.owned !== false; \}\)/, 'Modify offers only your own');
assert.match(js, /'Shared by ' \+ row\.shared_by_name/);
assert.match(js, /mr-share-all/); assert.match(js, /mr-share-link/); assert.match(js, /Withdraw the share/);
assert.match(js, /localStorage\.getItem\('ped_pending_share'\)/, 'a followed link is acted on once signed in');
assert.match(js, /Add it to your resources\?/);
assert.match(read('public/js/app.js'), /raw\.indexOf\('share=' \) === 0|raw\.indexOf\('share='\) === 0/);
assert.match(read('migrations/1781300000000_resource-shares.js'), /PRIMARY KEY \(resource_id, user_id\)/);
});