mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
add integration tests for the /sync endpoint (#423)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/423.md
Normal file
6
upcoming-release-notes/423.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [tcrasset]
|
||||
---
|
||||
|
||||
Add integration tests for the /sync endpoint
|
||||
Reference in New Issue
Block a user