// 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('an unbound token is upgraded on first successful use', () => { // Covers both older forms — plaintext, and encrypted before binding existed. // isEncrypted() is true for the latter, so gating on it would leave exactly // the rows that need rebinding untouched. assert.match(util, /if \(!cryptoUtil\.isBound\(place\.user\.nextcloud_token\)\)/); assert.match(util, /cryptoUtil\.encryptString\(place\.password, tokenContext\(userId\)\)/); });