add integration tests for the /sync endpoint (#423)

This commit is contained in:
Tom Crasset
2024-08-17 16:21:03 +02:00
committed by GitHub
parent 4ce7f55e0c
commit c16a8faa3f
3 changed files with 181 additions and 2 deletions

View File

@@ -12,8 +12,8 @@ import { SyncProtoBuf } from '@actual-app/crdt';
const app = express();
app.use(errorMiddleware);
app.use(express.json());
app.use(express.raw({ type: 'application/actual-sync' }));
app.use(express.json());
app.use(validateUserMiddleware);
export { app as handlers };
@@ -31,6 +31,7 @@ app.post('/sync', async (req, res) => {
try {
requestPb = SyncProtoBuf.SyncRequest.deserializeBinary(req.body);
} catch (e) {
console.log('Error parsing sync request', e);
res.status(500);
res.send({ status: 'error', reason: 'internal-error' });
return;
@@ -44,7 +45,11 @@ app.post('/sync', async (req, res) => {
let messages = requestPb.getMessagesList();
if (!since) {
throw new Error('`since` is required');
return res.status(422).send({
details: 'since-required',
reason: 'unprocessable-entity',
status: 'error',
});
}
let currentFiles = accountDb.all(

View File

@@ -3,6 +3,8 @@ import request from 'supertest';
import { handlers as app } from './app-sync.js';
import getAccountDb from './account-db.js';
import { getPathForUserFile } from './util/paths.js';
import { SyncProtoBuf } from '@actual-app/crdt';
import crypto from 'node:crypto';
describe('/download-user-file', () => {
describe('default version', () => {
@@ -137,3 +139,169 @@ describe('/delete-user-file', () => {
expect(rows[0].deleted).toBe(1);
});
});
describe('/sync', () => {
it('returns 401 if the user is not authenticated', async () => {
const res = await request(app).post('/sync');
expect(res.statusCode).toEqual(401);
expect(res.body).toEqual({
details: 'token-not-found',
reason: 'unauthorized',
status: 'error',
});
});
it('returns 200 and syncs successfully with correct file attributes', async () => {
const fileId = crypto.randomBytes(16).toString('hex');
const groupId = 'group-id';
const keyId = 'key-id';
const syncVersion = 2;
const encryptMeta = JSON.stringify({ keyId });
addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion);
const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
const res = await sendSyncRequest(syncRequest);
expect(res.statusCode).toEqual(200);
expect(res.headers['content-type']).toEqual('application/actual-sync');
expect(res.headers['x-actual-sync-method']).toEqual('simple');
});
it('returns 500 if the request body is invalid', async () => {
const res = await request(app)
.post('/sync')
.set('x-actual-token', 'valid-token')
// Content-Type is set correctly, but the body cannot be deserialized
.set('Content-Type', 'application/actual-sync')
.send('invalid-body');
expect(res.statusCode).toEqual(500);
expect(res.body).toEqual({
status: 'error',
reason: 'internal-error',
});
});
it('returns 422 if since is not provided', async () => {
const syncRequest = createMinimalSyncRequest(
'file-id',
'group-id',
'key-id',
);
syncRequest.setSince(undefined);
const res = await sendSyncRequest(syncRequest);
expect(res.statusCode).toEqual(422);
expect(res.body).toEqual({
status: 'error',
reason: 'unprocessable-entity',
details: 'since-required',
});
});
it('returns 400 if the file does not exist in the database', async () => {
const syncRequest = createMinimalSyncRequest(
'non-existant-file-id',
'group-id',
'key-id',
);
// We do not insert the file into the database, so it does not exist
const res = await sendSyncRequest(syncRequest);
expect(res.statusCode).toEqual(400);
expect(res.text).toEqual('file-not-found');
});
it('returns 400 if the file sync version is old', async () => {
const fileId = crypto.randomBytes(16).toString('hex');
const groupId = 'group-id';
const keyId = 'key-id';
const oldSyncVersion = 1; // Assuming SYNC_FORMAT_VERSION is 2
// Add a mock file with an old sync version
addMockFile(
fileId,
groupId,
keyId,
JSON.stringify({ keyId }),
oldSyncVersion,
);
const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
const res = await sendSyncRequest(syncRequest);
expect(res.statusCode).toEqual(400);
expect(res.text).toEqual('file-old-version');
});
it('returns 400 if the file needs to be uploaded (no group_id)', async () => {
const fileId = crypto.randomBytes(16).toString('hex');
const groupId = null; // No group ID
const keyId = 'key-id';
const syncVersion = 2;
addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion);
const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
const res = await sendSyncRequest(syncRequest);
expect(res.statusCode).toEqual(400);
expect(res.text).toEqual('file-needs-upload');
});
it('returns 400 if the file has a new encryption key', async () => {
const fileId = crypto.randomBytes(16).toString('hex');
const groupId = 'group-id';
const keyId = 'old-key-id';
const newKeyId = 'new-key-id';
const syncVersion = 2;
// Add a mock file with the old key
addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion);
// Create a sync request with the new key
const syncRequest = createMinimalSyncRequest(fileId, groupId, newKeyId);
const res = await sendSyncRequest(syncRequest);
expect(res.statusCode).toEqual(400);
expect(res.text).toEqual('file-has-new-key');
});
});
function addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion) {
getAccountDb().mutate(
'INSERT INTO files (id, group_id, encrypt_keyid, encrypt_meta, sync_version) VALUES (?, ?, ?,?, ?)',
[fileId, groupId, keyId, encryptMeta, syncVersion],
);
}
function createMinimalSyncRequest(fileId, groupId, keyId) {
const syncRequest = new SyncProtoBuf.SyncRequest();
syncRequest.setFileid(fileId);
syncRequest.setGroupid(groupId);
syncRequest.setKeyid(keyId);
syncRequest.setSince('2024-01-01T00:00:00.000Z');
syncRequest.setMessagesList([]);
return syncRequest;
}
async function sendSyncRequest(syncRequest) {
const serializedRequest = syncRequest.serializeBinary();
// Convert Uint8Array to Buffer
const bufferRequest = Buffer.from(serializedRequest);
const res = await request(app)
.post('/sync')
.set('x-actual-token', 'valid-token')
.set('Content-Type', 'application/actual-sync')
.send(bufferRequest);
return res;
}

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [tcrasset]
---
Add integration tests for the /sync endpoint