@actual-app/sync-server 25.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +12 -0
- package/README.md +19 -0
- package/app.js +11 -0
- package/babel.config.json +3 -0
- package/bin/@actual-app/sync-server +55 -0
- package/docker/alpine.Dockerfile +62 -0
- package/docker/ubuntu.Dockerfile +63 -0
- package/docker-compose.yml +29 -0
- package/jest.config.json +19 -0
- package/jest.global-setup.js +101 -0
- package/jest.global-teardown.js +6 -0
- package/migrations/1694360000000-create-folders.js +25 -0
- package/migrations/1694360479680-create-account-db.js +30 -0
- package/migrations/1694362247011-create-secret-table.js +16 -0
- package/migrations/1702667624000-rename-nordigen-secrets.js +19 -0
- package/migrations/1718889148000-openid.js +41 -0
- package/migrations/1719409568000-multiuser.js +116 -0
- package/package.json +64 -0
- package/src/account-db.js +239 -0
- package/src/accounts/openid.js +361 -0
- package/src/accounts/password.js +149 -0
- package/src/app-account.js +155 -0
- package/src/app-admin.js +410 -0
- package/src/app-admin.test.js +381 -0
- package/src/app-gocardless/README.md +198 -0
- package/src/app-gocardless/app-gocardless.js +274 -0
- package/src/app-gocardless/bank-factory.js +91 -0
- package/src/app-gocardless/banks/abanca_caglesmm.js +22 -0
- package/src/app-gocardless/banks/abnamro_abnanl2a.js +57 -0
- package/src/app-gocardless/banks/american_express_aesudef1.js +40 -0
- package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +31 -0
- package/src/app-gocardless/banks/bank.interface.ts +51 -0
- package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +39 -0
- package/src/app-gocardless/banks/bankinter_bkbkesmm.js +24 -0
- package/src/app-gocardless/banks/belfius_gkccbebb.js +17 -0
- package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +61 -0
- package/src/app-gocardless/banks/bnp_be_gebabebb.js +73 -0
- package/src/app-gocardless/banks/cbc_cregbebb.js +34 -0
- package/src/app-gocardless/banks/commerzbank_cobadeff.js +51 -0
- package/src/app-gocardless/banks/danskebank_dabno22.js +39 -0
- package/src/app-gocardless/banks/direkt_heladef1822.js +18 -0
- package/src/app-gocardless/banks/easybank_bawaatww.js +50 -0
- package/src/app-gocardless/banks/entercard_swednokk.js +40 -0
- package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +46 -0
- package/src/app-gocardless/banks/hype_hyeeit22.js +74 -0
- package/src/app-gocardless/banks/ing_ingbrobu.js +70 -0
- package/src/app-gocardless/banks/ing_ingddeff.js +47 -0
- package/src/app-gocardless/banks/ing_pl_ingbplpw.js +46 -0
- package/src/app-gocardless/banks/integration-bank.js +115 -0
- package/src/app-gocardless/banks/isybank_itbbitmm.js +18 -0
- package/src/app-gocardless/banks/kbc_kredbebb.js +33 -0
- package/src/app-gocardless/banks/lhv-lhvbee22.js +36 -0
- package/src/app-gocardless/banks/mbank_retail_brexplpw.js +56 -0
- package/src/app-gocardless/banks/nationwide_naiagb21.js +46 -0
- package/src/app-gocardless/banks/nbg_ethngraaxxx.js +51 -0
- package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +74 -0
- package/src/app-gocardless/banks/revolut_revolt21.js +37 -0
- package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +28 -0
- package/src/app-gocardless/banks/seb_kort_bank_ab.js +58 -0
- package/src/app-gocardless/banks/seb_privat.js +29 -0
- package/src/app-gocardless/banks/sparnord_spnodk22.js +24 -0
- package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +61 -0
- package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +30 -0
- package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +19 -0
- package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +50 -0
- package/src/app-gocardless/banks/swedbank_habalv22.js +47 -0
- package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +21 -0
- package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +61 -0
- package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +53 -0
- package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +22 -0
- package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +34 -0
- package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +110 -0
- package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +54 -0
- package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +206 -0
- package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +302 -0
- package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +202 -0
- package/src/app-gocardless/banks/tests/integration_bank.spec.js +158 -0
- package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +38 -0
- package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +68 -0
- package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +171 -0
- package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +105 -0
- package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +48 -0
- package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +42 -0
- package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +133 -0
- package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +256 -0
- package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +102 -0
- package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +57 -0
- package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +54 -0
- package/src/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +36 -0
- package/src/app-gocardless/banks/virgin_nrnbgb22.js +39 -0
- package/src/app-gocardless/errors.js +84 -0
- package/src/app-gocardless/gocardless-node.types.ts +497 -0
- package/src/app-gocardless/gocardless.types.ts +93 -0
- package/src/app-gocardless/link.html +18 -0
- package/src/app-gocardless/services/gocardless-service.js +620 -0
- package/src/app-gocardless/services/tests/fixtures.js +181 -0
- package/src/app-gocardless/services/tests/gocardless-service.spec.js +537 -0
- package/src/app-gocardless/tests/bank-factory.spec.js +20 -0
- package/src/app-gocardless/tests/utils.spec.js +162 -0
- package/src/app-gocardless/util/handle-error.js +16 -0
- package/src/app-gocardless/utils.js +45 -0
- package/src/app-openid.js +108 -0
- package/src/app-pluggyai/app-pluggyai.js +215 -0
- package/src/app-pluggyai/pluggyai-service.js +120 -0
- package/src/app-secrets.js +61 -0
- package/src/app-simplefin/app-simplefin.js +418 -0
- package/src/app-sync/errors.js +13 -0
- package/src/app-sync/services/files-service.js +243 -0
- package/src/app-sync/tests/services/files-service.test.js +250 -0
- package/src/app-sync/validation.js +77 -0
- package/src/app-sync.js +391 -0
- package/src/app-sync.test.js +877 -0
- package/src/app.js +145 -0
- package/src/config-types.ts +44 -0
- package/src/db.js +58 -0
- package/src/load-config.js +307 -0
- package/src/migrations.js +36 -0
- package/src/run-migrations.js +8 -0
- package/src/scripts/disable-openid.js +44 -0
- package/src/scripts/enable-openid.js +53 -0
- package/src/scripts/health-check.js +23 -0
- package/src/scripts/reset-password.js +51 -0
- package/src/secrets.test.js +83 -0
- package/src/services/secrets-service.js +94 -0
- package/src/services/user-service.js +272 -0
- package/src/sql/messages.sql +9 -0
- package/src/sync-simple.js +95 -0
- package/src/util/hash.js +5 -0
- package/src/util/middlewares.js +62 -0
- package/src/util/paths.js +13 -0
- package/src/util/payee-name.js +45 -0
- package/src/util/prompt.js +88 -0
- package/src/util/title/index.js +59 -0
- package/src/util/title/lower-case.js +93 -0
- package/src/util/title/specials.js +21 -0
- package/src/util/validate-user.js +68 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { SyncProtoBuf } from '@actual-app/crdt';
|
|
5
|
+
import request from 'supertest';
|
|
6
|
+
|
|
7
|
+
import { getAccountDb } from './account-db.js';
|
|
8
|
+
import { handlers as app } from './app-sync.js';
|
|
9
|
+
import { getPathForUserFile } from './util/paths.js';
|
|
10
|
+
|
|
11
|
+
const ADMIN_ROLE = 'ADMIN';
|
|
12
|
+
|
|
13
|
+
const createUser = (userId, userName, role, owner = 0, enabled = 1) => {
|
|
14
|
+
getAccountDb().mutate(
|
|
15
|
+
'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)',
|
|
16
|
+
[userId, userName, `${userName} display`, enabled, owner, role],
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('/user-get-key', () => {
|
|
21
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
22
|
+
const res = await request(app).post('/user-get-key');
|
|
23
|
+
|
|
24
|
+
expect(res.statusCode).toEqual(401);
|
|
25
|
+
expect(res.body).toEqual({
|
|
26
|
+
details: 'token-not-found',
|
|
27
|
+
reason: 'unauthorized',
|
|
28
|
+
status: 'error',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns encryption key details for a given fileId', async () => {
|
|
33
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
34
|
+
const encrypt_salt = 'test-salt';
|
|
35
|
+
const encrypt_keyid = 'test-key-id';
|
|
36
|
+
const encrypt_test = 'test-encrypt-test';
|
|
37
|
+
|
|
38
|
+
getAccountDb().mutate(
|
|
39
|
+
'INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test, owner) VALUES (?, ?, ?, ?, ?)',
|
|
40
|
+
[fileId, encrypt_salt, encrypt_keyid, encrypt_test, 'genericAdmin'],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const res = await request(app)
|
|
44
|
+
.post('/user-get-key')
|
|
45
|
+
.set('x-actual-token', 'valid-token')
|
|
46
|
+
.send({ fileId });
|
|
47
|
+
|
|
48
|
+
expect(res.statusCode).toEqual(200);
|
|
49
|
+
expect(res.body).toEqual({
|
|
50
|
+
status: 'ok',
|
|
51
|
+
data: {
|
|
52
|
+
id: encrypt_keyid,
|
|
53
|
+
salt: encrypt_salt,
|
|
54
|
+
test: encrypt_test,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns 400 if the file is not found', async () => {
|
|
60
|
+
const res = await request(app)
|
|
61
|
+
.post('/user-get-key')
|
|
62
|
+
.set('x-actual-token', 'valid-token')
|
|
63
|
+
.send({ fileId: 'non-existent-file-id' });
|
|
64
|
+
|
|
65
|
+
expect(res.statusCode).toEqual(400);
|
|
66
|
+
expect(res.text).toBe('file-not-found');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('/user-create-key', () => {
|
|
71
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
72
|
+
const res = await request(app).post('/user-create-key');
|
|
73
|
+
|
|
74
|
+
expect(res.statusCode).toEqual(401);
|
|
75
|
+
expect(res.body).toEqual({
|
|
76
|
+
details: 'token-not-found',
|
|
77
|
+
reason: 'unauthorized',
|
|
78
|
+
status: 'error',
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns 400 if the file is not found', async () => {
|
|
83
|
+
const res = await request(app)
|
|
84
|
+
.post('/user-create-key')
|
|
85
|
+
.set('x-actual-token', 'valid-token')
|
|
86
|
+
.send({ fileId: 'non-existent-file-id' });
|
|
87
|
+
|
|
88
|
+
expect(res.statusCode).toEqual(400);
|
|
89
|
+
expect(res.text).toBe('file not found');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('creates a new encryption key for the file', async () => {
|
|
93
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
94
|
+
|
|
95
|
+
const old_encrypt_salt = 'old-salt';
|
|
96
|
+
const old_encrypt_keyid = 'old-key';
|
|
97
|
+
const old_encrypt_test = 'old-encrypt-test';
|
|
98
|
+
const encrypt_salt = 'test-salt';
|
|
99
|
+
const encrypt_keyid = 'test-key-id';
|
|
100
|
+
const encrypt_test = 'test-encrypt-test';
|
|
101
|
+
|
|
102
|
+
getAccountDb().mutate(
|
|
103
|
+
'INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test) VALUES (?, ?, ?, ?)',
|
|
104
|
+
[fileId, old_encrypt_salt, old_encrypt_keyid, old_encrypt_test],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const res = await request(app)
|
|
108
|
+
.post('/user-create-key')
|
|
109
|
+
.set('x-actual-token', 'valid-token')
|
|
110
|
+
.send({
|
|
111
|
+
fileId,
|
|
112
|
+
keyId: encrypt_keyid,
|
|
113
|
+
keySalt: encrypt_salt,
|
|
114
|
+
testContent: encrypt_test,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(res.statusCode).toEqual(200);
|
|
118
|
+
expect(res.body).toEqual({ status: 'ok' });
|
|
119
|
+
|
|
120
|
+
const rows = getAccountDb().all(
|
|
121
|
+
'SELECT encrypt_salt, encrypt_keyid, encrypt_test FROM files WHERE id = ?',
|
|
122
|
+
[fileId],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(rows[0].encrypt_salt).toEqual(encrypt_salt);
|
|
126
|
+
expect(rows[0].encrypt_keyid).toEqual(encrypt_keyid);
|
|
127
|
+
expect(rows[0].encrypt_test).toEqual(encrypt_test);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('/reset-user-file', () => {
|
|
132
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
133
|
+
const res = await request(app).post('/reset-user-file');
|
|
134
|
+
|
|
135
|
+
expect(res.statusCode).toEqual(401);
|
|
136
|
+
expect(res.body).toEqual({
|
|
137
|
+
details: 'token-not-found',
|
|
138
|
+
reason: 'unauthorized',
|
|
139
|
+
status: 'error',
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('resets the user file and deletes the group file', async () => {
|
|
144
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
145
|
+
const groupId = 'test-group-id';
|
|
146
|
+
|
|
147
|
+
// Use addMockFile to insert a mock file into the database
|
|
148
|
+
getAccountDb().mutate(
|
|
149
|
+
'INSERT INTO files (id, group_id, deleted, owner) VALUES (?, ?, FALSE, ?)',
|
|
150
|
+
[fileId, groupId, 'genericAdmin'],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
getAccountDb().mutate(
|
|
154
|
+
'INSERT INTO user_access (file_id, user_id) VALUES (?, ?)',
|
|
155
|
+
[fileId, 'genericAdmin'],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const res = await request(app)
|
|
159
|
+
.post('/reset-user-file')
|
|
160
|
+
.set('x-actual-token', 'valid-token')
|
|
161
|
+
.send({ fileId });
|
|
162
|
+
|
|
163
|
+
expect(res.statusCode).toEqual(200);
|
|
164
|
+
expect(res.body).toEqual({ status: 'ok' });
|
|
165
|
+
|
|
166
|
+
// Verify that the file is marked as deleted
|
|
167
|
+
const rows = getAccountDb().all('SELECT group_id FROM files WHERE id = ?', [
|
|
168
|
+
fileId,
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
expect(rows[0].group_id).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns 400 if the file is not found', async () => {
|
|
175
|
+
const res = await request(app)
|
|
176
|
+
.post('/reset-user-file')
|
|
177
|
+
.set('x-actual-token', 'valid-token')
|
|
178
|
+
.send({ fileId: 'non-existent-file-id' });
|
|
179
|
+
|
|
180
|
+
expect(res.statusCode).toEqual(400);
|
|
181
|
+
expect(res.text).toBe('User or file not found');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('/upload-user-file', () => {
|
|
186
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
187
|
+
const res = await request(app).post('/upload-user-file');
|
|
188
|
+
|
|
189
|
+
expect(res.statusCode).toEqual(401);
|
|
190
|
+
expect(res.body).toEqual({
|
|
191
|
+
details: 'token-not-found',
|
|
192
|
+
reason: 'unauthorized',
|
|
193
|
+
status: 'error',
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('returns 400 if x-actual-name header is missing', async () => {
|
|
198
|
+
const res = await request(app)
|
|
199
|
+
.post('/upload-user-file')
|
|
200
|
+
.set('x-actual-token', 'valid-token')
|
|
201
|
+
.set('x-actual-file-id', 'test-file-id')
|
|
202
|
+
.send('file content');
|
|
203
|
+
|
|
204
|
+
expect(res.statusCode).toEqual(400);
|
|
205
|
+
expect(res.text).toBe('single x-actual-name is required');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('returns 400 if fileId is missing', async () => {
|
|
209
|
+
const content = Buffer.from('file content');
|
|
210
|
+
const res = await request(app)
|
|
211
|
+
.post('/upload-user-file')
|
|
212
|
+
.set('Content-Type', 'application/encrypted-file')
|
|
213
|
+
.set('x-actual-token', 'valid-token')
|
|
214
|
+
.set('x-actual-name', 'test-file')
|
|
215
|
+
.send(content);
|
|
216
|
+
|
|
217
|
+
expect(res.statusCode).toEqual(400);
|
|
218
|
+
expect(res.text).toBe('fileId is required');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('uploads a new file successfully', async () => {
|
|
222
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
223
|
+
const fileName = 'test-file.txt';
|
|
224
|
+
const fileContent = 'test file content';
|
|
225
|
+
const fileContentBuffer = Buffer.from(fileContent);
|
|
226
|
+
const syncVersion = 2;
|
|
227
|
+
const encryptMeta = JSON.stringify({ keyId: 'key-id' });
|
|
228
|
+
|
|
229
|
+
// Verify that the file does not exist before upload
|
|
230
|
+
const rowsBefore = getAccountDb().all('SELECT * FROM files WHERE id = ?', [
|
|
231
|
+
fileId,
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
expect(rowsBefore.length).toBe(0);
|
|
235
|
+
|
|
236
|
+
const res = await request(app)
|
|
237
|
+
.post('/upload-user-file')
|
|
238
|
+
.set('Content-Type', 'application/encrypted-file')
|
|
239
|
+
.set('x-actual-token', 'valid-token')
|
|
240
|
+
.set('x-actual-name', fileName)
|
|
241
|
+
.set('x-actual-file-id', fileId)
|
|
242
|
+
.set('x-actual-format', syncVersion.toString())
|
|
243
|
+
.set('x-actual-encrypt-meta', encryptMeta)
|
|
244
|
+
.send(fileContentBuffer);
|
|
245
|
+
|
|
246
|
+
expect(res.statusCode).toEqual(200);
|
|
247
|
+
expect(res.body).toEqual({ status: 'ok', groupId: expect.any(String) });
|
|
248
|
+
|
|
249
|
+
const receivedGroupid = res.body.groupId;
|
|
250
|
+
// Verify that the file exists in the accountDb
|
|
251
|
+
const rowsAfter = getAccountDb().all('SELECT * FROM files WHERE id = ?', [
|
|
252
|
+
fileId,
|
|
253
|
+
]);
|
|
254
|
+
expect(rowsAfter.length).toBe(1);
|
|
255
|
+
expect(rowsAfter[0].id).toEqual(fileId);
|
|
256
|
+
expect(rowsAfter[0].group_id).toEqual(receivedGroupid);
|
|
257
|
+
expect(rowsAfter[0].sync_version).toEqual(syncVersion);
|
|
258
|
+
expect(rowsAfter[0].name).toEqual(fileName);
|
|
259
|
+
expect(rowsAfter[0].encrypt_meta).toEqual(encryptMeta);
|
|
260
|
+
|
|
261
|
+
// Verify that the file was written to the file system
|
|
262
|
+
const filePath = getPathForUserFile(fileId);
|
|
263
|
+
const writtenContent = await fs.promises.readFile(filePath, 'utf8');
|
|
264
|
+
expect(writtenContent).toEqual(fileContent);
|
|
265
|
+
|
|
266
|
+
// Clean up the file
|
|
267
|
+
await fs.promises.unlink(filePath);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('uploads and updates an existing file successfully', async () => {
|
|
271
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
272
|
+
const oldGroupId = null; //sync state was reset
|
|
273
|
+
const oldFileName = 'old-test-file.txt';
|
|
274
|
+
const newFileName = 'new-test-file.txt';
|
|
275
|
+
const oldFileContent = 'old file content';
|
|
276
|
+
const newFileContent = 'new file content';
|
|
277
|
+
const oldSyncVersion = 1;
|
|
278
|
+
const newSyncVersion = 2;
|
|
279
|
+
const oldKeyId = 'old-key-id';
|
|
280
|
+
const oldEncryptMeta = JSON.stringify({ keyId: oldKeyId });
|
|
281
|
+
const newEncryptMeta = JSON.stringify({
|
|
282
|
+
keyId: oldKeyId,
|
|
283
|
+
sentinelValue: 1,
|
|
284
|
+
}); //keep the same key, but change other things
|
|
285
|
+
|
|
286
|
+
// Create the old file version
|
|
287
|
+
getAccountDb().mutate(
|
|
288
|
+
'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_keyid) VALUES (?, ?, ?, ?, ?, ?)',
|
|
289
|
+
[
|
|
290
|
+
fileId,
|
|
291
|
+
oldGroupId,
|
|
292
|
+
oldSyncVersion,
|
|
293
|
+
oldFileName,
|
|
294
|
+
oldEncryptMeta,
|
|
295
|
+
oldKeyId,
|
|
296
|
+
],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await fs.writeFile(getPathForUserFile(fileId), oldFileContent, err => {
|
|
300
|
+
if (err) throw err;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const res = await request(app)
|
|
304
|
+
.post('/upload-user-file')
|
|
305
|
+
.set('Content-Type', 'application/encrypted-file')
|
|
306
|
+
.set('x-actual-token', 'valid-token')
|
|
307
|
+
.set('x-actual-file-id', fileId)
|
|
308
|
+
.set('x-actual-name', newFileName)
|
|
309
|
+
.set('x-actual-format', newSyncVersion.toString())
|
|
310
|
+
.set('x-actual-encrypt-meta', newEncryptMeta)
|
|
311
|
+
.send(Buffer.from(newFileContent));
|
|
312
|
+
|
|
313
|
+
expect(res.statusCode).toEqual(200);
|
|
314
|
+
expect(res.body).toEqual({ status: 'ok', groupId: expect.any(String) });
|
|
315
|
+
|
|
316
|
+
const receivedGroupid = res.body.groupId;
|
|
317
|
+
|
|
318
|
+
// Verify that the file was updated in the accountDb
|
|
319
|
+
const rowsAfter = getAccountDb().all('SELECT * FROM files WHERE id = ?', [
|
|
320
|
+
fileId,
|
|
321
|
+
]);
|
|
322
|
+
expect(rowsAfter.length).toBe(1);
|
|
323
|
+
expect(rowsAfter[0].id).toEqual(fileId);
|
|
324
|
+
expect(rowsAfter[0].group_id).toEqual(receivedGroupid);
|
|
325
|
+
expect(rowsAfter[0].sync_version).toEqual(newSyncVersion);
|
|
326
|
+
expect(rowsAfter[0].name).toEqual(newFileName);
|
|
327
|
+
expect(rowsAfter[0].encrypt_meta).toEqual(newEncryptMeta);
|
|
328
|
+
|
|
329
|
+
// Verify that the file was written to the file system
|
|
330
|
+
const filePath = getPathForUserFile(fileId);
|
|
331
|
+
const writtenContent = await fs.promises.readFile(filePath, 'utf8');
|
|
332
|
+
expect(writtenContent).toEqual(newFileContent);
|
|
333
|
+
|
|
334
|
+
// Clean up the file
|
|
335
|
+
await fs.promises.unlink(filePath);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('returns 400 if the file is part of an old group', async () => {
|
|
339
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
340
|
+
const groupId = 'old-group-id';
|
|
341
|
+
const fileName = 'test-file.txt';
|
|
342
|
+
const keyId = 'key-id';
|
|
343
|
+
const syncVersion = 2;
|
|
344
|
+
|
|
345
|
+
// Add a mock file with the old group ID
|
|
346
|
+
addMockFile(
|
|
347
|
+
fileId,
|
|
348
|
+
'current-group-id',
|
|
349
|
+
keyId,
|
|
350
|
+
JSON.stringify({ keyId }),
|
|
351
|
+
syncVersion,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const res = await request(app)
|
|
355
|
+
.post('/upload-user-file')
|
|
356
|
+
.set('Content-Type', 'application/encrypted-file')
|
|
357
|
+
.set('x-actual-token', 'valid-token')
|
|
358
|
+
.set('x-actual-file-id', fileId)
|
|
359
|
+
.set('x-actual-group-id', groupId)
|
|
360
|
+
.set('x-actual-name', fileName);
|
|
361
|
+
|
|
362
|
+
expect(res.statusCode).toEqual(400);
|
|
363
|
+
expect(res.text).toEqual('file-has-reset');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('returns 400 if the file has a new encryption key', async () => {
|
|
367
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
368
|
+
const groupId = 'group-id';
|
|
369
|
+
const fileName = 'test-file.txt';
|
|
370
|
+
const oldKeyId = 'old-key-id';
|
|
371
|
+
const newKeyId = 'new-key-id';
|
|
372
|
+
const syncVersion = 2;
|
|
373
|
+
|
|
374
|
+
// Add a mock file with the new key
|
|
375
|
+
addMockFile(
|
|
376
|
+
fileId,
|
|
377
|
+
groupId,
|
|
378
|
+
newKeyId,
|
|
379
|
+
JSON.stringify({ newKeyId }),
|
|
380
|
+
syncVersion,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const res = await request(app)
|
|
384
|
+
.post('/upload-user-file')
|
|
385
|
+
.set('Content-Type', 'application/encrypted-file')
|
|
386
|
+
.set('x-actual-token', 'valid-token')
|
|
387
|
+
.set('x-actual-file-id', fileId)
|
|
388
|
+
.set('x-actual-group-id', groupId)
|
|
389
|
+
.set('x-actual-name', fileName)
|
|
390
|
+
.set('x-actual-encrypt-meta', JSON.stringify({ keyId: oldKeyId }));
|
|
391
|
+
|
|
392
|
+
expect(res.statusCode).toEqual(400);
|
|
393
|
+
expect(res.text).toEqual('file-has-new-key');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('/download-user-file', () => {
|
|
398
|
+
describe('default version', () => {
|
|
399
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
400
|
+
const res = await request(app).get('/download-user-file');
|
|
401
|
+
|
|
402
|
+
expect(res.statusCode).toEqual(401);
|
|
403
|
+
expect(res.body).toEqual({
|
|
404
|
+
details: 'token-not-found',
|
|
405
|
+
reason: 'unauthorized',
|
|
406
|
+
status: 'error',
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('returns 401 if the user is invalid', async () => {
|
|
411
|
+
const res = await request(app)
|
|
412
|
+
.get('/download-user-file')
|
|
413
|
+
.set('x-actual-token', 'invalid-token');
|
|
414
|
+
|
|
415
|
+
expect(res.statusCode).toEqual(401);
|
|
416
|
+
expect(res.body).toEqual({
|
|
417
|
+
details: 'token-not-found',
|
|
418
|
+
reason: 'unauthorized',
|
|
419
|
+
status: 'error',
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('returns 400 error if the file does not exist in the database', async () => {
|
|
424
|
+
const res = await request(app)
|
|
425
|
+
.get('/download-user-file')
|
|
426
|
+
.set('x-actual-token', 'valid-token')
|
|
427
|
+
.set('x-actual-file-id', 'non-existing-file-id');
|
|
428
|
+
|
|
429
|
+
expect(res.statusCode).toEqual(400);
|
|
430
|
+
expect(res.text).toBe('User or file not found');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('returns 500 error if the file does not exist on the filesystem', async () => {
|
|
434
|
+
getAccountDb().mutate(
|
|
435
|
+
'INSERT INTO files (id, deleted) VALUES (?, FALSE)',
|
|
436
|
+
['missing-fs-file'],
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const res = await request(app)
|
|
440
|
+
.get('/download-user-file')
|
|
441
|
+
.set('x-actual-token', 'valid-token')
|
|
442
|
+
.set('x-actual-file-id', 'missing-fs-file');
|
|
443
|
+
|
|
444
|
+
expect(res.statusCode).toEqual(404);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('returns an attachment file', async () => {
|
|
448
|
+
const fileContent = 'content';
|
|
449
|
+
fs.writeFileSync(getPathForUserFile('file-id'), fileContent);
|
|
450
|
+
getAccountDb().mutate(
|
|
451
|
+
'INSERT INTO files (id, deleted) VALUES (?, FALSE)',
|
|
452
|
+
['file-id'],
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const res = await request(app)
|
|
456
|
+
.get('/download-user-file')
|
|
457
|
+
.set('x-actual-token', 'valid-token')
|
|
458
|
+
.set('x-actual-file-id', 'file-id');
|
|
459
|
+
|
|
460
|
+
expect(res.statusCode).toEqual(200);
|
|
461
|
+
expect(res.headers).toEqual(
|
|
462
|
+
expect.objectContaining({
|
|
463
|
+
'content-disposition': 'attachment;filename=file-id',
|
|
464
|
+
'content-type': 'application/octet-stream',
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
expect(res.body).toBeInstanceOf(Buffer);
|
|
469
|
+
expect(res.body.toString('utf8')).toEqual(fileContent);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('/update-user-filename', () => {
|
|
475
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
476
|
+
const res = await request(app).post('/update-user-filename');
|
|
477
|
+
|
|
478
|
+
expect(res.statusCode).toEqual(401);
|
|
479
|
+
expect(res.body).toEqual({
|
|
480
|
+
details: 'token-not-found',
|
|
481
|
+
reason: 'unauthorized',
|
|
482
|
+
status: 'error',
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('returns 400 if the file is not found', async () => {
|
|
487
|
+
const res = await request(app)
|
|
488
|
+
.post('/update-user-filename')
|
|
489
|
+
.set('x-actual-token', 'valid-token')
|
|
490
|
+
.send({ fileId: 'non-existent-file-id', name: 'new-filename' });
|
|
491
|
+
|
|
492
|
+
expect(res.statusCode).toEqual(400);
|
|
493
|
+
expect(res.text).toBe('file not found');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('successfully updates the filename', async () => {
|
|
497
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
498
|
+
const oldName = 'old-filename';
|
|
499
|
+
const newName = 'new-filename';
|
|
500
|
+
|
|
501
|
+
// Insert a mock file into the database
|
|
502
|
+
getAccountDb().mutate(
|
|
503
|
+
'INSERT INTO files (id, name, deleted) VALUES (?, ?, FALSE)',
|
|
504
|
+
[fileId, oldName],
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const res = await request(app)
|
|
508
|
+
.post('/update-user-filename')
|
|
509
|
+
.set('x-actual-token', 'valid-token')
|
|
510
|
+
.send({ fileId, name: newName });
|
|
511
|
+
|
|
512
|
+
expect(res.statusCode).toEqual(200);
|
|
513
|
+
expect(res.body).toEqual({ status: 'ok' });
|
|
514
|
+
|
|
515
|
+
// Verify that the filename was updated
|
|
516
|
+
const rows = getAccountDb().all('SELECT name FROM files WHERE id = ?', [
|
|
517
|
+
fileId,
|
|
518
|
+
]);
|
|
519
|
+
|
|
520
|
+
expect(rows[0].name).toEqual(newName);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe('/list-user-files', () => {
|
|
525
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
526
|
+
const res = await request(app).get('/list-user-files');
|
|
527
|
+
|
|
528
|
+
expect(res.statusCode).toEqual(401);
|
|
529
|
+
expect(res.body).toEqual({
|
|
530
|
+
details: 'token-not-found',
|
|
531
|
+
reason: 'unauthorized',
|
|
532
|
+
status: 'error',
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('returns a list of user files for an authenticated user', async () => {
|
|
537
|
+
createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1);
|
|
538
|
+
const fileId1 = crypto.randomBytes(16).toString('hex');
|
|
539
|
+
const fileId2 = crypto.randomBytes(16).toString('hex');
|
|
540
|
+
const fileName1 = 'file1.txt';
|
|
541
|
+
const fileName2 = 'file2.txt';
|
|
542
|
+
|
|
543
|
+
// Insert mock files into the database
|
|
544
|
+
getAccountDb().mutate(
|
|
545
|
+
'INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)',
|
|
546
|
+
[fileId1, fileName1, ''],
|
|
547
|
+
);
|
|
548
|
+
getAccountDb().mutate(
|
|
549
|
+
'INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)',
|
|
550
|
+
[fileId2, fileName2, ''],
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const res = await request(app)
|
|
554
|
+
.get('/list-user-files')
|
|
555
|
+
.set('x-actual-token', 'valid-token');
|
|
556
|
+
|
|
557
|
+
expect(res.statusCode).toEqual(200);
|
|
558
|
+
expect(res.body).toEqual(
|
|
559
|
+
expect.objectContaining({
|
|
560
|
+
status: 'ok',
|
|
561
|
+
data: expect.arrayContaining([
|
|
562
|
+
expect.objectContaining({
|
|
563
|
+
deleted: 0,
|
|
564
|
+
fileId: fileId1,
|
|
565
|
+
groupId: null,
|
|
566
|
+
name: fileName1,
|
|
567
|
+
encryptKeyId: null,
|
|
568
|
+
}),
|
|
569
|
+
expect.objectContaining({
|
|
570
|
+
deleted: 0,
|
|
571
|
+
fileId: fileId2,
|
|
572
|
+
groupId: null,
|
|
573
|
+
name: fileName2,
|
|
574
|
+
encryptKeyId: null,
|
|
575
|
+
}),
|
|
576
|
+
]),
|
|
577
|
+
}),
|
|
578
|
+
);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('/get-user-file-info', () => {
|
|
583
|
+
it('returns file info for a valid fileId', async () => {
|
|
584
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
585
|
+
const groupId = 'test-group-id';
|
|
586
|
+
const fileInfo = {
|
|
587
|
+
id: fileId,
|
|
588
|
+
group_id: groupId,
|
|
589
|
+
name: 'test-file',
|
|
590
|
+
encrypt_meta: JSON.stringify({ key: 'value' }),
|
|
591
|
+
deleted: 0,
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
getAccountDb().mutate(
|
|
595
|
+
'INSERT INTO files (id, group_id, name, encrypt_meta, deleted) VALUES (?, ?, ?, ?, ?)',
|
|
596
|
+
[
|
|
597
|
+
fileInfo.id,
|
|
598
|
+
fileInfo.group_id,
|
|
599
|
+
fileInfo.name,
|
|
600
|
+
fileInfo.encrypt_meta,
|
|
601
|
+
fileInfo.deleted,
|
|
602
|
+
],
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const res = await request(app)
|
|
606
|
+
.get('/get-user-file-info')
|
|
607
|
+
.set('x-actual-token', 'valid-token')
|
|
608
|
+
.set('x-actual-file-id', fileId)
|
|
609
|
+
.send();
|
|
610
|
+
|
|
611
|
+
expect(res.statusCode).toEqual(200);
|
|
612
|
+
|
|
613
|
+
expect(res.body).toEqual({
|
|
614
|
+
status: 'ok',
|
|
615
|
+
data: {
|
|
616
|
+
deleted: fileInfo.deleted,
|
|
617
|
+
fileId: fileInfo.id,
|
|
618
|
+
groupId: fileInfo.group_id,
|
|
619
|
+
name: fileInfo.name,
|
|
620
|
+
encryptMeta: { key: 'value' },
|
|
621
|
+
usersWithAccess: [],
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('returns error if the file is not found', async () => {
|
|
627
|
+
const fileId = 'non-existent-file-id';
|
|
628
|
+
|
|
629
|
+
const res = await request(app)
|
|
630
|
+
.get('/get-user-file-info')
|
|
631
|
+
.set('x-actual-token', 'valid-token')
|
|
632
|
+
.set('x-actual-file-id', fileId);
|
|
633
|
+
|
|
634
|
+
expect(res.statusCode).toEqual(400);
|
|
635
|
+
expect(res.body).toEqual({ status: 'error', reason: 'file-not-found' });
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('returns error if the user is not authenticated', async () => {
|
|
639
|
+
// Simulate an unauthenticated request by not setting the necessary headers
|
|
640
|
+
const res = await request(app).get('/get-user-file-info');
|
|
641
|
+
|
|
642
|
+
expect(res.statusCode).toEqual(401);
|
|
643
|
+
expect(res.body).toEqual({
|
|
644
|
+
status: 'error',
|
|
645
|
+
reason: 'unauthorized',
|
|
646
|
+
details: 'token-not-found',
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
describe('/delete-user-file', () => {
|
|
652
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
653
|
+
const res = await request(app).post('/delete-user-file');
|
|
654
|
+
|
|
655
|
+
expect(res.statusCode).toEqual(401);
|
|
656
|
+
expect(res.body).toEqual({
|
|
657
|
+
details: 'token-not-found',
|
|
658
|
+
reason: 'unauthorized',
|
|
659
|
+
status: 'error',
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// it returns 422 if the fileId is not provided
|
|
664
|
+
it('returns 422 if the fileId is not provided', async () => {
|
|
665
|
+
const res = await request(app)
|
|
666
|
+
.post('/delete-user-file')
|
|
667
|
+
.set('x-actual-token', 'valid-token');
|
|
668
|
+
|
|
669
|
+
expect(res.statusCode).toEqual(422);
|
|
670
|
+
expect(res.body).toEqual({
|
|
671
|
+
details: 'fileId-required',
|
|
672
|
+
reason: 'unprocessable-entity',
|
|
673
|
+
status: 'error',
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('returns 400 if the file does not exist', async () => {
|
|
678
|
+
const res = await request(app)
|
|
679
|
+
.post('/delete-user-file')
|
|
680
|
+
.set('x-actual-token', 'valid-token')
|
|
681
|
+
.send({ fileId: 'non-existing-file-id' });
|
|
682
|
+
|
|
683
|
+
expect(res.statusCode).toEqual(400);
|
|
684
|
+
expect(res.text).toEqual('file-not-found');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('marks the file as deleted', async () => {
|
|
688
|
+
const accountDb = getAccountDb();
|
|
689
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
690
|
+
|
|
691
|
+
// Insert a file into the database
|
|
692
|
+
accountDb.mutate(
|
|
693
|
+
'INSERT OR IGNORE INTO files (id, deleted) VALUES (?, FALSE)',
|
|
694
|
+
[fileId],
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
const res = await request(app)
|
|
698
|
+
.post('/delete-user-file')
|
|
699
|
+
.set('x-actual-token', 'valid-token')
|
|
700
|
+
.send({ fileId });
|
|
701
|
+
|
|
702
|
+
expect(res.statusCode).toEqual(200);
|
|
703
|
+
expect(res.body).toEqual({ status: 'ok' });
|
|
704
|
+
|
|
705
|
+
// Verify that the file is marked as deleted
|
|
706
|
+
const rows = accountDb.all('SELECT deleted FROM files WHERE id = ?', [
|
|
707
|
+
fileId,
|
|
708
|
+
]);
|
|
709
|
+
expect(rows[0].deleted).toBe(1);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe('/sync', () => {
|
|
714
|
+
it('returns 401 if the user is not authenticated', async () => {
|
|
715
|
+
const res = await request(app).post('/sync');
|
|
716
|
+
|
|
717
|
+
expect(res.statusCode).toEqual(401);
|
|
718
|
+
expect(res.body).toEqual({
|
|
719
|
+
details: 'token-not-found',
|
|
720
|
+
reason: 'unauthorized',
|
|
721
|
+
status: 'error',
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('returns 200 and syncs successfully with correct file attributes', async () => {
|
|
726
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
727
|
+
const groupId = 'group-id';
|
|
728
|
+
const keyId = 'key-id';
|
|
729
|
+
const syncVersion = 2;
|
|
730
|
+
const encryptMeta = JSON.stringify({ keyId });
|
|
731
|
+
|
|
732
|
+
addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion);
|
|
733
|
+
|
|
734
|
+
const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
|
|
735
|
+
|
|
736
|
+
const res = await sendSyncRequest(syncRequest);
|
|
737
|
+
|
|
738
|
+
expect(res.statusCode).toEqual(200);
|
|
739
|
+
expect(res.headers['content-type']).toEqual('application/actual-sync');
|
|
740
|
+
expect(res.headers['x-actual-sync-method']).toEqual('simple');
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('returns 500 if the request body is invalid', async () => {
|
|
744
|
+
const res = await request(app)
|
|
745
|
+
.post('/sync')
|
|
746
|
+
.set('x-actual-token', 'valid-token')
|
|
747
|
+
// Content-Type is set correctly, but the body cannot be deserialized
|
|
748
|
+
.set('Content-Type', 'application/actual-sync')
|
|
749
|
+
.send('invalid-body');
|
|
750
|
+
|
|
751
|
+
expect(res.statusCode).toEqual(500);
|
|
752
|
+
expect(res.body).toEqual({
|
|
753
|
+
status: 'error',
|
|
754
|
+
reason: 'internal-error',
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('returns 422 if since is not provided', async () => {
|
|
759
|
+
const syncRequest = createMinimalSyncRequest(
|
|
760
|
+
'file-id',
|
|
761
|
+
'group-id',
|
|
762
|
+
'key-id',
|
|
763
|
+
);
|
|
764
|
+
syncRequest.setSince(undefined);
|
|
765
|
+
|
|
766
|
+
const res = await sendSyncRequest(syncRequest);
|
|
767
|
+
|
|
768
|
+
expect(res.statusCode).toEqual(422);
|
|
769
|
+
expect(res.body).toEqual({
|
|
770
|
+
status: 'error',
|
|
771
|
+
reason: 'unprocessable-entity',
|
|
772
|
+
details: 'since-required',
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('returns 400 if the file does not exist in the database', async () => {
|
|
777
|
+
const syncRequest = createMinimalSyncRequest(
|
|
778
|
+
'non-existant-file-id',
|
|
779
|
+
'group-id',
|
|
780
|
+
'key-id',
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
// We do not insert the file into the database, so it does not exist
|
|
784
|
+
|
|
785
|
+
const res = await sendSyncRequest(syncRequest);
|
|
786
|
+
|
|
787
|
+
expect(res.statusCode).toEqual(400);
|
|
788
|
+
expect(res.text).toEqual('file-not-found');
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it('returns 400 if the file sync version is old', async () => {
|
|
792
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
793
|
+
const groupId = 'group-id';
|
|
794
|
+
const keyId = 'key-id';
|
|
795
|
+
const oldSyncVersion = 1; // Assuming SYNC_FORMAT_VERSION is 2
|
|
796
|
+
|
|
797
|
+
// Add a mock file with an old sync version
|
|
798
|
+
addMockFile(
|
|
799
|
+
fileId,
|
|
800
|
+
groupId,
|
|
801
|
+
keyId,
|
|
802
|
+
JSON.stringify({ keyId }),
|
|
803
|
+
oldSyncVersion,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
|
|
807
|
+
|
|
808
|
+
const res = await sendSyncRequest(syncRequest);
|
|
809
|
+
|
|
810
|
+
expect(res.statusCode).toEqual(400);
|
|
811
|
+
expect(res.text).toEqual('file-old-version');
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('returns 400 if the file needs to be uploaded (no group_id)', async () => {
|
|
815
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
816
|
+
const groupId = null; // No group ID
|
|
817
|
+
const keyId = 'key-id';
|
|
818
|
+
const syncVersion = 2;
|
|
819
|
+
|
|
820
|
+
addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion);
|
|
821
|
+
|
|
822
|
+
const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
|
|
823
|
+
|
|
824
|
+
const res = await sendSyncRequest(syncRequest);
|
|
825
|
+
|
|
826
|
+
expect(res.statusCode).toEqual(400);
|
|
827
|
+
expect(res.text).toEqual('file-needs-upload');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('returns 400 if the file has a new encryption key', async () => {
|
|
831
|
+
const fileId = crypto.randomBytes(16).toString('hex');
|
|
832
|
+
const groupId = 'group-id';
|
|
833
|
+
const keyId = 'old-key-id';
|
|
834
|
+
const newKeyId = 'new-key-id';
|
|
835
|
+
const syncVersion = 2;
|
|
836
|
+
|
|
837
|
+
// Add a mock file with the old key
|
|
838
|
+
addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion);
|
|
839
|
+
|
|
840
|
+
// Create a sync request with the new key
|
|
841
|
+
const syncRequest = createMinimalSyncRequest(fileId, groupId, newKeyId);
|
|
842
|
+
const res = await sendSyncRequest(syncRequest);
|
|
843
|
+
|
|
844
|
+
expect(res.statusCode).toEqual(400);
|
|
845
|
+
expect(res.text).toEqual('file-has-new-key');
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
function addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion) {
|
|
850
|
+
getAccountDb().mutate(
|
|
851
|
+
'INSERT INTO files (id, group_id, encrypt_keyid, encrypt_meta, sync_version, owner) VALUES (?, ?, ?,?, ?, ?)',
|
|
852
|
+
[fileId, groupId, keyId, encryptMeta, syncVersion, 'genericAdmin'],
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function createMinimalSyncRequest(fileId, groupId, keyId) {
|
|
857
|
+
const syncRequest = new SyncProtoBuf.SyncRequest();
|
|
858
|
+
syncRequest.setFileid(fileId);
|
|
859
|
+
syncRequest.setGroupid(groupId);
|
|
860
|
+
syncRequest.setKeyid(keyId);
|
|
861
|
+
syncRequest.setSince('2024-01-01T00:00:00.000Z');
|
|
862
|
+
syncRequest.setMessagesList([]);
|
|
863
|
+
return syncRequest;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function sendSyncRequest(syncRequest) {
|
|
867
|
+
const serializedRequest = syncRequest.serializeBinary();
|
|
868
|
+
// Convert Uint8Array to Buffer
|
|
869
|
+
const bufferRequest = Buffer.from(serializedRequest);
|
|
870
|
+
|
|
871
|
+
const res = await request(app)
|
|
872
|
+
.post('/sync')
|
|
873
|
+
.set('x-actual-token', 'valid-token')
|
|
874
|
+
.set('Content-Type', 'application/actual-sync')
|
|
875
|
+
.send(bufferRequest);
|
|
876
|
+
return res;
|
|
877
|
+
}
|