SPB Git

Repository Public

files.test.ts 22.7 KB 286 lines typescript

1import { execFileSync } from 'node:child_process';2import { randomBytes } from 'node:crypto';3import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';4import { join } from 'node:path';5import { afterAll, beforeAll, describe, expect, it } from 'vitest';6import { config } from '../src/config.ts';7import { sql } from '../src/db.ts';8import { Client, closeAll, sha, signup, work } from './harness.ts';910afterAll(closeAll);11let a: Awaited<ReturnType<typeof signup>>, b: Awaited<ReturnType<typeof signup>>;12beforeAll(async () => { a = await signup(); b = await signup(); });13const mkdir = async (c: Client, parentId: string, name: string) => (await c.post('/api/v1/folders', { parentId, name })).body;14const ls = async (c: Client, id: string) => (await c.get(`/api/v1/folders/${id}`)).body;15const walk = (dir: string): string[] => readdirSync(dir, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? walk(join(dir, e.name)) : [join(dir, e.name)]));1617describe('uploads', () => {18  it('stores a multi-chunk file, verifies its SHA-256 and serves it back with Range', async () => {19    const data = randomBytes(5 * 1024 * 1024 + 123);20    const up = await a.client.upload(a.rootId, 'gros.bin', data, { chunk: 1024 * 1024 });21    expect(up.status).toBe(200);22    expect(up.body.file).toMatchObject({ name: 'gros.bin', size: data.length, sha256: sha(data) });23    const id = up.body.file.id;24    const full = await a.client.get(`/api/v1/files/${id}/content?download=1`);25    expect(full.status).toBe(200);26    expect(full.raw.equals(data)).toBe(true);27    expect(full.headers['content-disposition']).toContain('attachment');28    expect(full.headers['content-security-policy']).toContain('sandbox');29    const mid = await a.client.get(`/api/v1/files/${id}/content`, { range: 'bytes=65530-200005' }); // crosses several cipher blocks30    expect(mid.status).toBe(206);31    expect(mid.headers['content-range']).toBe(`bytes 65530-200005/${data.length}`);32    expect(mid.raw.equals(data.subarray(65530, 200006))).toBe(true);33    const tail = await a.client.get(`/api/v1/files/${id}/content`, { range: 'bytes=-100' });34    expect(tail.raw.equals(data.subarray(data.length - 100))).toBe(true);35    expect((await a.client.get(`/api/v1/files/${id}/content`, { range: `bytes=${data.length}-` })).status).toBe(416);36    expect((await a.client.get(`/api/v1/files/${id}/content`, { 'if-none-match': `"${sha(data)}"` })).status).toBe(304);37  });38  it('never writes plaintext to the blob store, and detects tampering', async () => {39    const marker = Buffer.from(`PLAINTEXT-MARKER-${randomBytes(8).toString('hex')}-`.repeat(4000));40    const up = await a.client.upload(a.rootId, 'secret.txt', marker);41    const [o] = await sql<{ storage_key: string }[]>`select o.storage_key from storage_objects o where o.sha256 = ${sha(marker)}`;42    const blob = join(config.blobDir, o!.storage_key);43    expect(readFileSync(blob).includes(marker.subarray(0, 40))).toBe(false);44    expect(statSync(blob).mode & 0o077).toBe(0);45    for (const f of walk(config.blobDir)) expect(readFileSync(f).includes(marker.subarray(0, 40))).toBe(false);46    expect(readdirSync(config.tmpDir).filter((n) => n.startsWith('up-'))).toHaveLength(0); // temp file removed47    // flip one byte of the ciphertext: the read must fail, never return altered content48    const raw = readFileSync(blob); raw[100] = raw[100]! ^ 1; writeFileSync(blob, raw);49    const res = await a.client.get(`/api/v1/files/${up.body.file.id}/content?download=1`).catch((e) => ({ status: 599, raw: Buffer.alloc(0), error: e }));50    expect(res.status === 599 || !res.raw.equals(marker)).toBe(true);51    raw[100] = raw[100]! ^ 1; writeFileSync(blob, raw);52    expect((await a.client.get(`/api/v1/files/${up.body.file.id}/content?download=1`)).raw.equals(marker)).toBe(true);53  });54  it('resumes after an interruption from the offset the server reports', async () => {55    const data = randomBytes(3 * 1024 * 1024);56    const u = (await a.client.post('/api/v1/uploads', { folderId: a.rootId, name: 'reprise.bin', size: data.length })).body;57    expect((await a.client.patch(`/api/v1/uploads/${u.id}?offset=0`, data.subarray(0, 1024 * 1024))).body.received).toBe(1024 * 1024);58    // "connection lost": a new client instance asks where to continue59    const later = new Client(a.client.auth); await later.login();60    expect((await later.get(`/api/v1/uploads/${u.id}`)).body.received).toBe(1024 * 1024);61    // a chunk at the wrong place is refused and tells the right offset62    const wrong = await later.patch(`/api/v1/uploads/${u.id}?offset=${2 * 1024 * 1024}`, data.subarray(2 * 1024 * 1024));63    expect(wrong.status).toBe(409); expect(wrong.body.received).toBe(1024 * 1024);64    // a corrupted chunk is refused and leaves the offset untouched65    const corrupt = await later.patch(`/api/v1/uploads/${u.id}?offset=${1024 * 1024}`, data.subarray(1024 * 1024, 2 * 1024 * 1024), { 'x-chunk-sha256': sha(Buffer.from('other')) });66    expect(corrupt.status).toBe(422);67    expect((await later.get(`/api/v1/uploads/${u.id}`)).body.received).toBe(1024 * 1024);68    expect((await later.post(`/api/v1/uploads/${u.id}/complete`)).status).toBe(409); // not all bytes yet69    expect((await later.patch(`/api/v1/uploads/${u.id}?offset=${1024 * 1024}`, data.subarray(1024 * 1024))).body.received).toBe(data.length);70    expect((await later.post(`/api/v1/uploads/${u.id}/complete`, { sha256: sha(Buffer.from('not this')) })).status).toBe(422); // whole-file checksum71    const done = await later.post(`/api/v1/uploads/${u.id}/complete`, { sha256: sha(data) });72    expect(done.body.file.sha256).toBe(sha(data));73    expect((await later.post(`/api/v1/uploads/${u.id}/complete`, { sha256: sha(data) })).body.file.id).toBe(done.body.file.id); // idempotent74  });75  it('refuses bytes beyond the declared size and cancels cleanly', async () => {76    const u = (await a.client.post('/api/v1/uploads', { folderId: a.rootId, name: 'petit.bin', size: 10 })).body;77    expect((await a.client.patch(`/api/v1/uploads/${u.id}?offset=0`, randomBytes(11))).status).toBe(413);78    expect((await a.client.get(`/api/v1/uploads/${u.id}`)).body.received).toBe(0);79    expect((await a.client.del(`/api/v1/uploads/${u.id}`)).status).toBe(200);80    expect((await a.client.patch(`/api/v1/uploads/${u.id}?offset=0`, randomBytes(10))).status).toBe(410);81    expect(readdirSync(config.tmpDir).filter((n) => n.includes(u.id))).toHaveLength(0);82  });83  it('serialises concurrent chunks for one upload: exactly one wins each offset', async () => {84    const data = randomBytes(64 * 1024);85    const u = (await a.client.post('/api/v1/uploads', { folderId: a.rootId, name: 'course.bin', size: data.length })).body;86    const res = await Promise.all(Array.from({ length: 6 }, () => a.client.patch(`/api/v1/uploads/${u.id}?offset=0`, data)));87    expect(res.filter((r) => r.status === 200)).toHaveLength(1);88    expect(res.filter((r) => r.status === 409)).toHaveLength(5);89    const done = await a.client.post(`/api/v1/uploads/${u.id}/complete`, { sha256: sha(data) });90    expect(done.body.file.size).toBe(data.length);91  });92  it('handles name collisions explicitly: fail, rename or new version — never a silent overwrite', async () => {93    const dir = await mkdir(a.client, a.rootId, 'Collisions');94    const v1 = Buffer.from('version 1'), v2 = Buffer.from('version 2 plus longue');95    const first = await a.client.upload(dir.id, 'Rapport.pdf', v1);96    const fail = await a.client.upload(dir.id, 'rapport.PDF', v2, { onConflict: 'fail' }); // case-insensitive collision97    expect(fail.status).toBe(409); expect(fail.body).toMatchObject({ error: 'name_conflict', name: 'rapport.PDF' });98    const renamed = await a.client.upload(dir.id, 'Rapport.pdf', v2, { onConflict: 'rename' });99    expect(renamed.body.file.name).toBe('Rapport (2).pdf');100    const versioned = await a.client.upload(dir.id, 'Rapport.pdf', v2, { onConflict: 'version' });101    expect(versioned.body.file.id).toBe(first.body.file.id);102    const detail = (await a.client.get(`/api/v1/files/${first.body.file.id}`)).body;103    expect(detail.versions.map((v: any) => v.seq)).toEqual([2, 1]);104    expect((await a.client.get(`/api/v1/files/${first.body.file.id}/content?download=1`)).raw.equals(v2)).toBe(true);105    expect((await a.client.get(`/api/v1/files/${first.body.file.id}/content?download=1&v=${detail.versions[1].id}`)).raw.equals(v1)).toBe(true);106    // restoring version 1 records a version 3 with the old content107    expect((await a.client.post(`/api/v1/files/${first.body.file.id}/versions/${detail.versions[1].id}/restore`)).body.sha256).toBe(sha(v1));108    expect((await a.client.get(`/api/v1/files/${first.body.file.id}`)).body.versions).toHaveLength(3);109    // a folder and a file cannot share a name110    expect((await a.client.post('/api/v1/folders', { parentId: dir.id, name: 'rapport.pdf' })).status).toBe(409);111  });112  it('resolves a race of identical names: all uploads land, none is lost', async () => {113    const dir = await mkdir(a.client, a.rootId, 'Course');114    const res = await Promise.all(Array.from({ length: 8 }, (_, i) => a.client.upload(dir.id, 'photo.jpg', Buffer.from(`contenu ${i}`))));115    expect(res.every((r) => r.status === 200)).toBe(true);116    const names = (await ls(a.client, dir.id)).files.map((f: any) => f.name).sort();117    expect(new Set(names).size).toBe(8);118    expect(names).toContain('photo.jpg'); expect(names).toContain('photo (8).jpg');119    const folders = await Promise.all(Array.from({ length: 6 }, () => a.client.post('/api/v1/folders', { parentId: dir.id, name: 'Même nom' })));120    expect(folders.filter((r) => r.status === 200)).toHaveLength(1);121    expect(folders.filter((r) => r.status === 409)).toHaveLength(5);122  });123  it('creates the folder structure of a folder upload and neutralises hostile paths', async () => {124    const dir = await mkdir(a.client, a.rootId, 'Import');125    await a.client.upload(dir.id, 'a.txt', Buffer.from('a'), { relativePath: 'Projet/src/a.txt' });126    await a.client.upload(dir.id, 'b.txt', Buffer.from('b'), { relativePath: 'Projet/src/b.txt' });127    const evil = await a.client.upload(dir.id, '../../../evil.txt', Buffer.from('e'), { relativePath: '../../../../etc/evil.txt' });128    expect(evil.body.file.name).not.toMatch(/[/\\]/);129    const top = await ls(a.client, dir.id);130    expect(top.folders.map((f: any) => f.name).sort()).toEqual(['Projet', 'etc']);131    const projet = await ls(a.client, top.folders.find((f: any) => f.name === 'Projet').id);132    const src = await ls(a.client, projet.folders[0].id);133    expect(src.files.map((f: any) => f.name).sort()).toEqual(['a.txt', 'b.txt']);134    expect(src.breadcrumbs.map((c: any) => c.name)).toEqual(['', 'Import', 'Projet', 'src']);135    expect((await ls(a.client, a.rootId)).files.some((f: any) => f.name.includes('evil'))).toBe(false); // nothing escaped upward136  });137  it('enforces the quota, counting uploads in flight, and deduplicates within the account only', async () => {138    const small = await signup({ quotaBytes: 1024 * 1024 });139    expect((await small.client.post('/api/v1/uploads', { folderId: small.rootId, name: 'trop.bin', size: 2 * 1024 * 1024 })).body.error).toBe('quota_exceeded');140    const first = await small.client.post('/api/v1/uploads', { folderId: small.rootId, name: 'a.bin', size: 700 * 1024 });141    expect(first.status).toBe(200);142    expect((await small.client.post('/api/v1/uploads', { folderId: small.rootId, name: 'b.bin', size: 700 * 1024 })).body.error).toBe('quota_exceeded'); // reserved by the first143    await small.client.del(`/api/v1/uploads/${first.body.id}`);144    const data = randomBytes(400 * 1024);145    expect((await small.client.upload(small.rootId, 'un.bin', data)).status).toBe(200);146    expect((await small.client.upload(small.rootId, 'deux.bin', data)).status).toBe(200); // same bytes: one stored object147    expect((await small.client.get('/api/v1/auth/state')).body.quota.used).toBe(data.length);148    const dup = (await small.client.get('/api/v1/duplicates')).body.groups;149    expect(dup).toHaveLength(1); expect(dup[0].files).toHaveLength(2);150    // another account uploading the same bytes pays for them and learns nothing151    const other = await signup({ quotaBytes: 1024 * 1024 });152    expect((await other.client.upload(other.rootId, 'copie.bin', data)).status).toBe(200);153    expect((await other.client.get('/api/v1/auth/state')).body.quota.used).toBe(data.length);154    expect((await other.client.get('/api/v1/duplicates')).body.groups).toHaveLength(0);155    const objs = await sql`select owner_id from storage_objects where sha256 = ${sha(data)}`;156    expect(objs).toHaveLength(2);157  });158});159160describe('explorer', () => {161  it('moves, copies, renames; refuses cycles and collisions', async () => {162    const top = await mkdir(a.client, a.rootId, 'Organisation');163    const x = await mkdir(a.client, top.id, 'X'), y = await mkdir(a.client, top.id, 'Y');164    const inner = await mkdir(a.client, x.id, 'Inner');165    const f = (await a.client.upload(x.id, 'note.txt', Buffer.from('note'))).body.file;166    expect((await a.client.post('/api/v1/move', { folderIds: [x.id], destinationId: inner.id })).body.error).toBe('move_into_self');167    expect((await a.client.post('/api/v1/move', { folderIds: [x.id], destinationId: x.id })).body.error).toBe('move_into_self');168    expect((await a.client.post('/api/v1/move', { fileIds: [f.id], destinationId: y.id })).status).toBe(200);169    expect((await ls(a.client, y.id)).files[0].name).toBe('note.txt');170    await a.client.upload(x.id, 'note.txt', Buffer.from('autre'));171    const back = await a.client.post('/api/v1/move', { fileIds: [f.id], destinationId: x.id });172    expect(back.status).toBe(409);173    expect((await a.client.post('/api/v1/move', { fileIds: [f.id], destinationId: x.id, onConflict: 'rename' })).status).toBe(200);174    expect((await ls(a.client, x.id)).files.map((q: any) => q.name).sort()).toEqual(['note (2).txt', 'note.txt']);175    expect((await a.client.patch(`/api/v1/files/${f.id}`, { name: 'note.txt' })).status).toBe(409);176    expect((await a.client.patch(`/api/v1/files/${f.id}`, { name: 'renommé.txt' })).body.name).toBe('renommé.txt');177    const copy = await a.client.post('/api/v1/copy', { folderIds: [x.id], destinationId: y.id });178    expect(copy.body).toEqual({ files: 2, folders: 2 });179    const copied = await ls(a.client, (await ls(a.client, y.id)).folders[0].id);180    expect(copied.files).toHaveLength(2); expect(copied.folders[0].name).toBe('Inner');181    expect((await a.client.post('/api/v1/copy', { folderIds: [x.id], destinationId: x.id })).status).toBe(200); // copy into itself terminates182    expect((await a.client.patch(`/api/v1/folders/${a.rootId}`, { name: 'x' })).status).toBe(400);183  });184  it('trashes, restores and purges whole subtrees; storage is reclaimed', async () => {185    const top = await mkdir(a.client, a.rootId, 'Corbeille-test');186    const sub = await mkdir(a.client, top.id, 'Sous');187    const data = randomBytes(200 * 1024);188    const f = (await a.client.upload(sub.id, 'doc.bin', data)).body.file;189    await a.client.post('/api/v1/trash', { folderIds: [top.id] });190    expect((await a.client.get(`/api/v1/folders/${sub.id}`)).status).toBe(404);191    expect((await a.client.get('/api/v1/search?q=doc.bin')).body.files).toHaveLength(0);192    const trash = (await a.client.get('/api/v1/trash')).body;193    expect(trash.folders.map((x: any) => x.id)).toEqual([top.id]); // one root, not every descendant194    // meanwhile the name is reused195    await mkdir(a.client, a.rootId, 'Corbeille-test');196    await a.client.post('/api/v1/restore', { folderIds: [top.id] });197    const names = (await ls(a.client, a.rootId)).folders.map((x: any) => x.name).filter((n: string) => n.startsWith('Corbeille-test')).sort();198    expect(names).toEqual(['Corbeille-test', 'Corbeille-test (2)']);199    expect((await ls(a.client, sub.id)).files[0].id).toBe(f.id);200    // purge201    await a.client.post('/api/v1/trash', { fileIds: [f.id] });202    const before = (await a.client.get('/api/v1/auth/state')).body.quota.used;203    expect((await a.client.post('/api/v1/purge', { fileIds: [sub.id] })).status).toBe(404); // not a trash root204    await a.client.post('/api/v1/purge', { fileIds: [f.id] });205    const [o] = await sql<{ storage_key: string }[]>`select storage_key from storage_objects where sha256 = ${sha(data)}`;206    await work();207    expect((await a.client.get('/api/v1/auth/state')).body.quota.used).toBe(before - data.length);208    expect(() => statSync(join(config.blobDir, o!.storage_key))).toThrow();209    // restoring an item whose folder is gone falls back to the root210    const g = (await a.client.upload(sub.id, 'orphelin.txt', Buffer.from('o'))).body.file;211    await a.client.post('/api/v1/trash', { fileIds: [g.id] });212    await a.client.post('/api/v1/trash', { folderIds: [sub.id] });213    await a.client.post('/api/v1/purge', { folderIds: [sub.id] });214    await a.client.post('/api/v1/restore', { fileIds: [g.id] });215    expect((await ls(a.client, a.rootId)).files.some((x: any) => x.id === g.id)).toBe(true);216  });217  it('searches by name without accents or case, with filters, tags, favourites and recents', async () => {218    const dir = await mkdir(a.client, a.rootId, 'Recherche');219    const f1 = (await a.client.upload(dir.id, 'Résumé été 2026.pdf', Buffer.from('%PDF-1.4 x'))).body.file;220    await a.client.upload(dir.id, 'budget.xlsx', Buffer.from('xx'));221    await a.client.upload(dir.id, '100%_fini.txt', Buffer.from('t'));222    expect((await a.client.get(`/api/v1/search?q=${encodeURIComponent('resume ETE')}`)).body.files.map((f: any) => f.id)).toEqual([f1.id]);223    expect((await a.client.get('/api/v1/search?q=&kind=sheet')).body.files.map((f: any) => f.name)).toContain('budget.xlsx');224    expect((await a.client.get(`/api/v1/search?q=${encodeURIComponent('100%_')}`)).body.files).toHaveLength(1); // wildcards are literal225    expect((await a.client.get(`/api/v1/search?q=${encodeURIComponent("' or 1=1 --")}`)).body.files).toHaveLength(0);226    const tag = (await a.client.post('/api/v1/tags', { name: 'Impôts', color: 'green' })).body;227    expect((await a.client.post('/api/v1/tags', { name: 'impôts' })).status).toBe(409);228    await a.client.patch(`/api/v1/files/${f1.id}`, { tagIds: [tag.id], starred: true });229    expect((await a.client.get(`/api/v1/search?tag=${tag.id}`)).body.files.map((f: any) => f.id)).toEqual([f1.id]);230    expect((await a.client.get('/api/v1/starred')).body.files.map((f: any) => f.id)).toContain(f1.id);231    expect((await a.client.get('/api/v1/recent')).body.files[0].name).toBe('100%_fini.txt');232    const foreignTag = (await b.client.post('/api/v1/tags', { name: 'Autre' })).body;233    expect((await a.client.patch(`/api/v1/files/${f1.id}`, { tagIds: [foreignTag.id] })).status).toBe(404);234  });235  it('exports a folder as a ZIP with its structure', async () => {236    const dir = await mkdir(a.client, a.rootId, 'Export');237    const sub = await mkdir(a.client, dir.id, 'Sous dossier');238    const one = randomBytes(70000), two = Buffer.from('deux');239    await a.client.upload(dir.id, 'un.bin', one); await a.client.upload(sub.id, 'deux é.txt', two);240    const zip = await a.client.get(`/api/v1/zip?folders=${dir.id}`);241    expect(zip.status).toBe(200); expect(zip.headers['content-type']).toBe('application/zip');242    const path = join('/tmp/sp-drive-test', 'export.zip'); writeFileSync(path, zip.raw);243    const listing = execFileSync('/usr/bin/unzip', ['-Z1', path]).toString().trim().split('\n').sort();244    expect(listing).toEqual(['Export/Sous dossier/deux é.txt', 'Export/un.bin']);245    expect(execFileSync('/usr/bin/unzip', ['-p', path, 'Export/un.bin'], { maxBuffer: 1e8 }).equals(one)).toBe(true);246  });247});248249describe('isolation between accounts (IDOR)', () => {250  it('answers 404 for every route given another account\'s identifiers', async () => {251    const dir = await mkdir(a.client, a.rootId, 'Privé A');252    const f = (await a.client.upload(dir.id, 'privé.txt', Buffer.from('secret de A'))).body.file;253    const detail = (await a.client.get(`/api/v1/files/${f.id}`)).body;254    const up = (await a.client.post('/api/v1/uploads', { folderId: dir.id, name: 'x', size: 5 })).body;255    const tag = (await a.client.post('/api/v1/tags', { name: 'Privé' })).body;256    const mine = (await b.client.upload(b.rootId, 'à-moi.txt', Buffer.from('b'))).body.file;257    const tries: [string, string, unknown?][] = [258      ['GET', `/api/v1/folders/${dir.id}`], ['GET', `/api/v1/folders/${dir.id}/children`], ['PATCH', `/api/v1/folders/${dir.id}`, { name: 'pris' }],259      ['POST', '/api/v1/folders', { parentId: dir.id, name: 'intrus' }], ['GET', `/api/v1/files/${f.id}`], ['PATCH', `/api/v1/files/${f.id}`, { name: 'pris.txt' }],260      ['GET', `/api/v1/files/${f.id}/content`], ['GET', `/api/v1/files/${f.id}/content?download=1`], ['GET', `/api/v1/files/${f.id}/preview/thumb`],261      ['GET', `/api/v1/files/${mine.id}/content?v=${detail.versions[0].id}`], // own file, foreign version id262      ['POST', `/api/v1/files/${mine.id}/versions/${detail.versions[0].id}/restore`], ['GET', `/api/v1/zip?files=${f.id}`], ['GET', `/api/v1/zip?folders=${dir.id}`],263      ['POST', '/api/v1/move', { fileIds: [f.id], destinationId: b.rootId }], ['POST', '/api/v1/move', { fileIds: [mine.id], destinationId: dir.id }],264      ['POST', '/api/v1/copy', { fileIds: [f.id], destinationId: b.rootId }], ['POST', '/api/v1/copy', { folderIds: [dir.id], destinationId: b.rootId }],265      ['POST', '/api/v1/trash', { fileIds: [f.id] }], ['POST', '/api/v1/trash', { folderIds: [dir.id] }], ['POST', '/api/v1/restore', { fileIds: [f.id] }], ['POST', '/api/v1/purge', { fileIds: [f.id] }],266      ['POST', '/api/v1/uploads', { folderId: dir.id, name: 'intrus.txt', size: 1 }], ['POST', '/api/v1/uploads', { folderId: 'root', name: 'v.txt', size: 1, fileId: f.id }],267      ['GET', `/api/v1/uploads/${up.id}`], ['PATCH', `/api/v1/uploads/${up.id}?offset=0`, Buffer.from('12345')], ['POST', `/api/v1/uploads/${up.id}/complete`], ['DELETE', `/api/v1/uploads/${up.id}`],268      ['PATCH', `/api/v1/tags/${tag.id}`, { name: 'pris' }], ['DELETE', `/api/v1/tags/${tag.id}`],269      ['POST', '/api/v1/shares', { fileId: f.id }], ['POST', '/api/v1/shares', { folderId: dir.id }], ['POST', '/api/v1/requests', { folderId: dir.id, title: 'Vol' }],270    ];271    for (const [method, url, body] of tries) {272      const r = await b.client.call(method, url, body);273      expect([method, url, r.status]).toEqual([method, url, 404]);274      expect(JSON.stringify(r.body ?? '')).not.toContain('secret de A');275    }276    // nothing changed for A, nothing appeared for B277    expect((await ls(a.client, dir.id)).files.map((x: any) => x.name)).toEqual(['privé.txt']);278    expect((await a.client.get(`/api/v1/uploads/${up.id}`)).body.received).toBe(0);279    expect((await b.client.get('/api/v1/search?q=priv')).body).toEqual({ folders: [], files: [] });280    expect((await b.client.get('/api/v1/recent')).body.files.every((x: any) => x.id !== f.id)).toBe(true);281    // and anonymous callers get 401 everywhere282    const anon = new Client();283    for (const [method, url, body] of tries) expect([url, (await anon.call(method, url, body)).status]).toEqual([url, 401]);284  });285});286
Files main
Menu

Theme