A resource was private with no way out but the author's own Nextcloud. Share opens reading — open, preview, download — to one person at a time by exact email (no account is ever listed) or to everyone signed in with one switch; what others share appears in your library marked "Shared by …". Writing never travels: modify, re-skin, delete and the share list stay the author's, every write still filtered on user_id, and the read routes go through one reader rule. Rows follow the resource and the person. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
41 lines
3.1 KiB
JavaScript
41 lines
3.1 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/shares'", "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');
|
|
}
|
|
assert.match(route, /SELECT id, name, email FROM users WHERE email = \? AND disabled IS NOT TRUE/);
|
|
assert.match(route, /No account with that email on this site/);
|
|
assert.match(route, /ON CONFLICT DO NOTHING/, 'sharing twice is not an error');
|
|
});
|
|
|
|
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-email/); assert.match(js, /Withdraw the share/);
|
|
assert.match(read('migrations/1781300000000_resource-shares.js'), /PRIMARY KEY \(resource_id, user_id\)/);
|
|
});
|