From d1213bc8c2e8aeccd74534b0adcd3a32e775b61f Mon Sep 17 00:00:00 2001 From: Tom Crasset <25140344+tcrasset@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:52:39 +0200 Subject: [PATCH] [Maintenance] improve testing utils and add delete-user-file test (#421) * improve testing utils and add delete-user-file test * remove linting errors * add release notes * match npm scripts naming style Co-authored-by: Matt Fiddaman * add raw middleware for /sync --------- Co-authored-by: Matt Fiddaman --- package.json | 4 +++ src/app-sync.js | 27 ++++++++++++--- src/app-sync.test.js | 62 +++++++++++++++++++++++++++++++++++ src/run-migrations.js | 8 +++++ upcoming-release-notes/421.md | 6 ++++ 5 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/run-migrations.js create mode 100644 upcoming-release-notes/421.md diff --git a/package.json b/package.json index 6992d9328..9cce1858d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,10 @@ "lint": "eslint . --max-warnings 0", "build": "tsc", "test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules --trace-warnings' jest --coverage", + "db:migrate": "NODE_ENV=development node src/run-migrations.js up", + "db:downgrade": "NODE_ENV=development node src/run-migrations.js down", + "db:test-migrate": "NODE_ENV=test node src/run-migrations.js up", + "db:test-downgrade": "NODE_ENV=test node src/run-migrations.js down", "types": "tsc --noEmit --incremental", "verify": "yarn lint && yarn types", "reset-password": "node src/scripts/reset-password.js", diff --git a/src/app-sync.js b/src/app-sync.js index 5d036c3ab..7bad89079 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -13,8 +13,13 @@ import { SyncProtoBuf } from '@actual-app/crdt'; const app = express(); app.use(errorMiddleware); +app.use(express.json()); +app.use(express.raw({ type: 'application/actual-sync' })); + export { app as handlers }; +const OK_RESPONSE = { status: 'ok' }; + // This is a version representing the internal format of sync // messages. When this changes, all sync files need to be reset. We // will check this version when syncing and notify the user if they @@ -160,7 +165,7 @@ app.post('/user-create-key', (req, res) => { [keySalt, keyId, testContent, fileId], ); - res.send(JSON.stringify({ status: 'ok' })); + res.send(OK_RESPONSE); }); app.post('/reset-user-file', async (req, res) => { @@ -190,7 +195,7 @@ app.post('/reset-user-file', async (req, res) => { } } - res.send(JSON.stringify({ status: 'ok' })); + res.send(OK_RESPONSE); }); app.post('/upload-user-file', async (req, res) => { @@ -333,7 +338,7 @@ app.post('/update-user-filename', (req, res) => { accountDb.mutate('UPDATE files SET name = ? WHERE id = ?', [name, fileId]); - res.send(JSON.stringify({ status: 'ok' })); + res.send(OK_RESPONSE); }); app.get('/list-user-files', (req, res) => { @@ -399,6 +404,20 @@ app.post('/delete-user-file', (req, res) => { let accountDb = getAccountDb(); let { fileId } = req.body; + if (!fileId) { + return res.status(422).send({ + details: 'fileId-required', + reason: 'unprocessable-entity', + status: 'error', + }); + } + + let rows = accountDb.all('SELECT * FROM files WHERE id = ?', [fileId]); + + if (rows.length === 0) { + return res.status(400).send('file-not-found'); + } + accountDb.mutate('UPDATE files SET deleted = TRUE WHERE id = ?', [fileId]); - res.send(JSON.stringify({ status: 'ok' })); + res.send(OK_RESPONSE); }); diff --git a/src/app-sync.test.js b/src/app-sync.test.js index c8d5e22f9..c99acf85f 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -75,3 +75,65 @@ describe('/download-user-file', () => { }); }); }); + +describe('/delete-user-file', () => { + it('returns 401 if the user is not authenticated', async () => { + const res = await request(app).post('/delete-user-file'); + + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual({ + details: 'token-not-found', + reason: 'unauthorized', + status: 'error', + }); + }); + + // it returns 422 if the fileId is not provided + it('returns 422 if the fileId is not provided', async () => { + const res = await request(app) + .post('/delete-user-file') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(422); + expect(res.body).toEqual({ + details: 'fileId-required', + reason: 'unprocessable-entity', + status: 'error', + }); + }); + + it('returns 400 if the file does not exist', async () => { + const res = await request(app) + .post('/delete-user-file') + .set('x-actual-token', 'valid-token') + .send({ fileId: 'non-existing-file-id' }); + + expect(res.statusCode).toEqual(400); + expect(res.text).toEqual('file-not-found'); + }); + + it('marks the file as deleted', async () => { + const accountDb = getAccountDb(); + const fileId = 'existing-file-id'; + + // Insert a file into the database + accountDb.mutate( + 'INSERT OR IGNORE INTO files (id, deleted) VALUES (?, FALSE)', + [fileId], + ); + + const res = await request(app) + .post('/delete-user-file') + .set('x-actual-token', 'valid-token') + .send({ fileId }); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + + // Verify that the file is marked as deleted + const rows = accountDb.all('SELECT deleted FROM files WHERE id = ?', [ + fileId, + ]); + expect(rows[0].deleted).toBe(1); + }); +}); diff --git a/src/run-migrations.js b/src/run-migrations.js new file mode 100644 index 000000000..b5ed26940 --- /dev/null +++ b/src/run-migrations.js @@ -0,0 +1,8 @@ +import run from './migrations.js'; + +const direction = process.argv[2] || 'up'; + +run(direction).catch((err) => { + console.error('Migration failed:', err); + process.exit(1); +}); diff --git a/upcoming-release-notes/421.md b/upcoming-release-notes/421.md new file mode 100644 index 000000000..fa35639c2 --- /dev/null +++ b/upcoming-release-notes/421.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [tcrasset] +--- + +Improve testing utils and add delete-user-file test \ No newline at end of file