Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 52s
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Android APK / Build signed APK (push) Successful in 1m56s
Forgejo Docker Build / Build Docker image (push) Successful in 11s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
A .pptx landing in someone's own storage is worth having; a markdown blob is not, and it is not what they would have downloaded. So this renders through exactly the path the download uses — stored deck, its figures, the chosen theme — and PUTs the bytes. The file never travels through the browser. Offered only when a Nextcloud is connected: an action that always fails is worse than one that is not offered. An article offers Word, a deck PowerPoint, and asking for slides from an article is refused with the reason. Putting a file in Nextcloud now lives in src/utils/nextcloudFiles.js. Two callers want it and neither should grow its own copy of the WebDAV dance — make the dated folder a segment at a time, PUT, migrate a legacy plaintext token — because it reaches into storage that is not ours and a second slightly different copy is how the two drift. It also replaces a route importing another route. Also: a model that leaves the roster now leaves every list that names it. clinical_assistant.allowed_models and the image roster are advisory copies of the roster, and a stale id there was invisible until someone asked a clinical question and the request failed at the gateway. Removing or disabling a model prunes it; clearing the roster clears them. Re-enabling deliberately does not re-allow it — that is a separate decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
67 lines
3.5 KiB
JavaScript
67 lines
3.5 KiB
JavaScript
// Sending a resource to Nextcloud pushes the rendered file, not the markdown.
|
|
// A .pptx landing in someone's own storage is the thing worth having; a text
|
|
// blob is not, and it is not what they would have downloaded.
|
|
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');
|
|
const route = read('src/routes/myResources.js');
|
|
const util = read('src/utils/nextcloudFiles.js');
|
|
const ui = read('public/js/myResources.js');
|
|
|
|
test('the file is rendered by the same path a download uses', () => {
|
|
const handler = route.slice(route.indexOf("router.post('/my-resources/:id/to-nextcloud'"));
|
|
assert.match(handler.slice(0, 2200), /documentExport\.render\(row\.markdown, row\.kind, format/);
|
|
assert.match(handler.slice(0, 2200), /deck: deck/, 'the stored deck, so it renders as designed');
|
|
assert.match(handler.slice(0, 2200), /collectFigures\(row\.image_ids/, 'with its figures');
|
|
// Never the markdown on its own.
|
|
assert.doesNotMatch(handler.slice(0, 2200), /send\([^)]*row\.markdown/);
|
|
});
|
|
|
|
test('it is scoped to the owner and refuses a format the resource cannot be', () => {
|
|
const handler = route.slice(route.indexOf("router.post('/my-resources/:id/to-nextcloud'"));
|
|
assert.match(handler.slice(0, 2200), /WHERE id = \? AND user_id = \?/);
|
|
assert.match(handler.slice(0, 2200), /An article has no slides/);
|
|
assert.match(handler.slice(0, 2200), /documentExport\.isSupported\(format\)/);
|
|
});
|
|
|
|
test('a disconnected Nextcloud says so, rather than failing as a server error', () => {
|
|
assert.match(util, /Nextcloud is not connected\. Connect it in Settings\./);
|
|
assert.match(util, /Nextcloud credentials are invalid\. Reconnect in Settings\./);
|
|
// Those carry a status the route passes through instead of flattening to 502.
|
|
const handler = route.slice(route.indexOf("router.post('/my-resources/:id/to-nextcloud'"));
|
|
assert.match(handler.slice(0, 2400), /if \(err\.statusCode\) return res\.status\(err\.statusCode\)/);
|
|
});
|
|
|
|
test('one module knows how to put a file in Nextcloud', () => {
|
|
// A route importing another route is what this replaced.
|
|
assert.match(route, /require\('\.\.\/utils\/nextcloudFiles'\)/);
|
|
assert.doesNotMatch(route, /require\('\.\/nextcloud'\)/);
|
|
assert.match(util, /async function send\(userId, name, bytes, contentType\)/);
|
|
});
|
|
|
|
test('the filename cannot traverse or confuse a filesystem', () => {
|
|
assert.match(util, /replace\(\/\[\^a-zA-Z0-9\._-\]\/g, '_'\)/);
|
|
assert.match(util, /\.slice\(0, 120\) \|\| 'resource'/, 'and cannot end up empty');
|
|
});
|
|
|
|
test('the folder tree is made a segment at a time, and an existing one is not an error', () => {
|
|
// WebDAV will not create a tree in one call; MKCOL on an existing folder is 405.
|
|
assert.match(util, /for \(var part of folder\.split\('\/'\)\.filter\(Boolean\)\)/);
|
|
assert.match(util, /already there, or the PUT below will report the real fault/);
|
|
});
|
|
|
|
test('the button appears only when there is a Nextcloud to send to', () => {
|
|
assert.match(route, /nextcloudConnected: Boolean/);
|
|
assert.match(ui, /if \(nextcloudConnected\) \{/);
|
|
assert.match(ui, /data-nextcloud|dataset\.nextcloud/);
|
|
// An article offers Word, a deck offers PowerPoint.
|
|
assert.match(ui, /row\.kind === 'article' \? 'docx' : 'pptx'/);
|
|
});
|
|
|
|
test('a legacy plaintext token is encrypted on first successful use', () => {
|
|
assert.match(util, /if \(!cryptoUtil\.isEncrypted\(place\.user\.nextcloud_token\)\)/);
|
|
assert.match(util, /cryptoUtil\.encryptString\(place\.password\)/);
|
|
});
|