snow-editor

small markdown and org-mode editor
Log | Files | Refs | README

api.test.js (6413B)


      1 import assert from 'node:assert';
      2 import { after, before, describe, test } from 'node:test';
      3 import { createApp } from '../src/app.js';
      4 import { getDb, initDb } from '../src/db.js';
      5 const ALLOWED_ORIGIN = 'http://localhost:41737';
      6 
      7 let server;
      8 let baseUrl;
      9 
     10 function api(path, { method = 'GET', headers = {}, body } = {}) {
     11   return fetch(`${baseUrl}${path}`, {
     12     method,
     13     headers: {
     14       'Content-Type': 'application/json',
     15       ...headers,
     16     },
     17     body: body === undefined ? undefined : JSON.stringify(body),
     18   });
     19 }
     20 
     21 async function readJson(res) {
     22   const text = await res.text();
     23   return text ? JSON.parse(text) : null;
     24 }
     25 
     26 async function createDocument(overrides = {}) {
     27   const res = await api('/api/documents', {
     28     method: 'POST',
     29     headers: { Origin: ALLOWED_ORIGIN },
     30     body: {
     31       title: 'Test doc',
     32       mode: 'markdown',
     33       content: '# Hello',
     34       expiresIn: '7d',
     35       ...overrides,
     36     },
     37   });
     38   assert.equal(res.status, 201);
     39   return readJson(res);
     40 }
     41 
     42 before(() => {
     43   initDb(':memory:');
     44   const app = createApp({
     45     shareAllowedOrigins: ALLOWED_ORIGIN,
     46   });
     47   server = app.listen(0);
     48   const { port } = server.address();
     49   baseUrl = `http://127.0.0.1:${port}`;
     50 });
     51 
     52 after(() => {
     53   server.close();
     54 });
     55 
     56 describe('Snow Editor API', () => {
     57   test('GET /api/health returns ok with db check', async () => {
     58     const res = await api('/api/health');
     59     const data = await readJson(res);
     60 
     61     assert.equal(res.status, 200);
     62     assert.equal(data.ok, true);
     63     assert.equal(data.db, 'ok');
     64     assert.equal(typeof data.uptime, 'number');
     65     assert.equal(data.version, '0.0.1');
     66   });
     67 
     68   test('POST /documents without Origin is rejected', async () => {
     69     const res = await api('/api/documents', {
     70       method: 'POST',
     71       body: {
     72         title: 'Blocked',
     73         mode: 'markdown',
     74         content: 'nope',
     75         expiresIn: '7d',
     76       },
     77     });
     78     const data = await readJson(res);
     79 
     80     assert.equal(res.status, 403);
     81     assert.equal(data.error, 'ORIGIN_NOT_ALLOWED');
     82   });
     83 
     84   test('POST /documents with allowed Origin succeeds', async () => {
     85     const data = await createDocument({ title: 'Allowed' });
     86     assert.ok(data.viewToken);
     87     assert.ok(data.editToken);
     88   });
     89 
     90   test('GET /view/:token returns document', async () => {
     91     const doc = await createDocument({ content: '# View me' });
     92     const res = await api(`/api/documents/view/${doc.viewToken}`);
     93     const data = await readJson(res);
     94 
     95     assert.equal(res.status, 200);
     96     assert.equal(data.content, '# View me');
     97   });
     98 
     99   test('POST lock acquires edit lock', async () => {
    100     const doc = await createDocument();
    101     const res = await api(`/api/documents/edit/${doc.editToken}/lock`, {
    102       method: 'POST',
    103       body: { clientId: 'client-a' },
    104     });
    105     const data = await readJson(res);
    106 
    107     assert.equal(res.status, 200);
    108     assert.equal(data.locked, true);
    109     assert.ok(data.lockToken);
    110   });
    111 
    112   test('second clientId on lock returns 423', async () => {
    113     const doc = await createDocument();
    114     await api(`/api/documents/edit/${doc.editToken}/lock`, {
    115       method: 'POST',
    116       body: { clientId: 'client-a' },
    117     });
    118 
    119     const res = await api(`/api/documents/edit/${doc.editToken}/lock`, {
    120       method: 'POST',
    121       body: { clientId: 'client-b' },
    122     });
    123     const data = await readJson(res);
    124 
    125     assert.equal(res.status, 423);
    126     assert.equal(data.error, 'DOCUMENT_LOCKED');
    127   });
    128 
    129   test('PUT without lock returns 403', async () => {
    130     const doc = await createDocument();
    131     const res = await api(`/api/documents/edit/${doc.editToken}`, {
    132       method: 'PUT',
    133       body: {
    134         clientId: 'ghost',
    135         lockToken: 'missing',
    136         title: 'Nope',
    137         mode: 'markdown',
    138         content: 'fail',
    139       },
    140     });
    141     const data = await readJson(res);
    142 
    143     assert.equal(res.status, 403);
    144     assert.equal(data.error, 'LOCK_REQUIRED');
    145   });
    146 
    147   test('expired document returns 410', async () => {
    148     const doc = await createDocument();
    149     const past = new Date(Date.now() - 60_000).toISOString();
    150     getDb()
    151       .prepare('UPDATE documents SET expires_at = ? WHERE edit_token = ?')
    152       .run(past, doc.editToken);
    153 
    154     const res = await api(`/api/documents/edit/${doc.editToken}`);
    155     const data = await readJson(res);
    156 
    157     assert.equal(res.status, 410);
    158     assert.equal(data.error, 'EXPIRED');
    159   });
    160 
    161   test('body larger than 1 MB returns 413', async () => {
    162     const doc = await createDocument();
    163     const lockRes = await api(`/api/documents/edit/${doc.editToken}/lock`, {
    164       method: 'POST',
    165       body: { clientId: 'big-body' },
    166     });
    167     const lock = await readJson(lockRes);
    168 
    169     const huge = 'x'.repeat(1024 * 1024 + 1);
    170     const res = await api(`/api/documents/edit/${doc.editToken}`, {
    171       method: 'PUT',
    172       body: {
    173         clientId: 'big-body',
    174         lockToken: lock.lockToken,
    175         title: 'Huge',
    176         mode: 'markdown',
    177         content: huge,
    178       },
    179     });
    180     const data = await readJson(res);
    181 
    182     assert.equal(res.status, 413);
    183     assert.equal(data.error, 'CONTENT_TOO_LARGE');
    184   });
    185 
    186   test('version list and restore require active lock', async () => {
    187     const doc = await createDocument({ content: 'v1' });
    188     const lockRes = await api(`/api/documents/edit/${doc.editToken}/lock`, {
    189       method: 'POST',
    190       body: { clientId: 'version-client' },
    191     });
    192     const lock = await readJson(lockRes);
    193 
    194     await api(`/api/documents/edit/${doc.editToken}`, {
    195       method: 'PUT',
    196       body: {
    197         clientId: 'version-client',
    198         lockToken: lock.lockToken,
    199         title: doc.title,
    200         mode: 'markdown',
    201         content: 'v2',
    202       },
    203     });
    204 
    205     const versionsRes = await api(
    206       `/api/documents/edit/${doc.editToken}/versions?clientId=version-client&lockToken=${lock.lockToken}`,
    207     );
    208     const versionsData = await readJson(versionsRes);
    209 
    210     assert.equal(versionsRes.status, 200);
    211     assert.ok(versionsData.versions.length >= 1);
    212 
    213     const versionId = versionsData.versions[0].id;
    214     const restoreRes = await api(
    215       `/api/documents/edit/${doc.editToken}/versions/${versionId}/restore`,
    216       {
    217         method: 'POST',
    218         body: {
    219           clientId: 'version-client',
    220           lockToken: lock.lockToken,
    221         },
    222       },
    223     );
    224     const restored = await readJson(restoreRes);
    225 
    226     assert.equal(restoreRes.status, 200);
    227     assert.equal(restored.content, 'v1');
    228   });
    229 });