pediatric-ai-scribe-v3/test/resource-sharing.test.js
Daniel d2a06b0fcf feat: modifying a resource is a job, the same as generating one
Modify held the request open for a library search, a PubMed search, a web
search and a restating model call. That is minutes, and a browser gives up
first — Firefox abandons a non-streaming fetch at five minutes, the same
failure generating was moved off the request to fix in ef574edd. The server
carried on and saved the result while the person watched an error, and closing
the tab killed the work outright.

POST /my-resources/:id/refine now records the request and answers 202 with the
job, exactly as /generate does. The writing moved into refineResource(), which
the job runner dispatches to by kind; the job list, the five-second polling,
the restart recovery and the three-in-flight cap are all the work they already
did, unchanged. Ownership is checked again inside refineResource because the
resource can be deleted while the job waits.

The page follows the job instead of the response. Reporting is unchanged — the
unchanged reply, what was seen and what was searched — it is only said from the
job list now, so it still reaches the person who asked for it after a reload.
2026-09-16 23:28:42 +02:00

52 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 s\.user_id IS NOT NULL\)/);
assert.doesNotMatch(route, /shares\/all|shared_with_all/, 'sharing is by link only');
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'", "async function refineResource(", "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.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, /);
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.doesNotMatch(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\)/);
});