@actual-app/sync-server 25.5.0 → 25.6.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.
Files changed (244) hide show
  1. package/{app.js → build/app.js} +4 -5
  2. package/build/bin/actual-server.js +101 -0
  3. package/build/migrations/1694360000000-create-folders.js +21 -0
  4. package/{migrations → build/migrations}/1694360479680-create-account-db.js +2 -4
  5. package/{migrations → build/migrations}/1694362247011-create-secret-table.js +2 -4
  6. package/build/migrations/1702667624000-rename-nordigen-secrets.js +9 -0
  7. package/{migrations → build/migrations}/1718889148000-openid.js +4 -10
  8. package/{migrations → build/migrations}/1719409568000-multiuser.js +10 -26
  9. package/build/src/account-db.js +182 -0
  10. package/build/src/accounts/openid.js +287 -0
  11. package/build/src/accounts/password.js +98 -0
  12. package/build/src/app-account.js +125 -0
  13. package/build/src/app-admin.js +317 -0
  14. package/build/src/app-admin.test.js +303 -0
  15. package/build/src/app-gocardless/app-gocardless.js +193 -0
  16. package/build/src/app-gocardless/bank-factory.js +84 -0
  17. package/build/src/app-gocardless/banks/abanca_caglesmm.js +17 -0
  18. package/build/src/app-gocardless/banks/abnamro_abnanl2a.js +37 -0
  19. package/build/src/app-gocardless/banks/american_express_aesudef1.js +32 -0
  20. package/build/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +22 -0
  21. package/build/src/app-gocardless/banks/bank.interface.js +1 -0
  22. package/build/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +25 -0
  23. package/build/src/app-gocardless/banks/bankinter_bkbkesmm.js +18 -0
  24. package/build/src/app-gocardless/banks/belfius_gkccbebb.js +13 -0
  25. package/build/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +48 -0
  26. package/build/src/app-gocardless/banks/bnp_be_gebabebb.js +64 -0
  27. package/build/src/app-gocardless/banks/boursobank_bousfrppxxx.js +73 -0
  28. package/build/src/app-gocardless/banks/cbc_cregbebb.js +27 -0
  29. package/build/src/app-gocardless/banks/commerzbank_cobadeff.js +43 -0
  30. package/build/src/app-gocardless/banks/danskebank_dabno22.js +26 -0
  31. package/build/src/app-gocardless/banks/direkt_heladef1822.js +13 -0
  32. package/build/src/app-gocardless/banks/easybank_bawaatww.js +42 -0
  33. package/build/src/app-gocardless/banks/entercard_swednokk.js +28 -0
  34. package/build/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +34 -0
  35. package/build/src/app-gocardless/banks/hype_hyeeit22.js +63 -0
  36. package/build/src/app-gocardless/banks/ing_ingbrobu.js +56 -0
  37. package/build/src/app-gocardless/banks/ing_ingddeff.js +34 -0
  38. package/build/src/app-gocardless/banks/ing_pl_ingbplpw.js +29 -0
  39. package/build/src/app-gocardless/banks/integration-bank.js +78 -0
  40. package/build/src/app-gocardless/banks/isybank_itbbitmm.js +13 -0
  41. package/build/src/app-gocardless/banks/kbc_kredbebb.js +26 -0
  42. package/build/src/app-gocardless/banks/lhv-lhvbee22.js +24 -0
  43. package/build/src/app-gocardless/banks/mbank_retail_brexplpw.js +41 -0
  44. package/build/src/app-gocardless/banks/nationwide_naiagb21.js +32 -0
  45. package/build/src/app-gocardless/banks/nbg_ethngraaxxx.js +39 -0
  46. package/build/src/app-gocardless/banks/norwegian_xx_norwnok1.js +61 -0
  47. package/build/src/app-gocardless/banks/revolut_revolt21.js +20 -0
  48. package/build/src/app-gocardless/banks/sandboxfinance_sfin0000.js +21 -0
  49. package/build/src/app-gocardless/banks/seb_kort_bank_ab.js +63 -0
  50. package/build/src/app-gocardless/banks/seb_privat.js +19 -0
  51. package/build/src/app-gocardless/banks/sparnord_spnodk22.js +19 -0
  52. package/build/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +48 -0
  53. package/build/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +25 -0
  54. package/build/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +14 -0
  55. package/build/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +36 -0
  56. package/build/src/app-gocardless/banks/swedbank_habalv22.js +30 -0
  57. package/build/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +17 -0
  58. package/build/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +45 -0
  59. package/build/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +41 -0
  60. package/build/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +16 -0
  61. package/build/src/app-gocardless/banks/tests/boursobank_bousfrppxxx.spec.js +102 -0
  62. package/build/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +24 -0
  63. package/build/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +105 -0
  64. package/build/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +36 -0
  65. package/build/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +159 -0
  66. package/build/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +267 -0
  67. package/build/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +186 -0
  68. package/build/src/app-gocardless/banks/tests/integration_bank.spec.js +127 -0
  69. package/build/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +24 -0
  70. package/build/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +50 -0
  71. package/build/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +156 -0
  72. package/build/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +64 -0
  73. package/build/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +36 -0
  74. package/build/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +30 -0
  75. package/build/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +112 -0
  76. package/build/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +214 -0
  77. package/build/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +60 -0
  78. package/build/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +45 -0
  79. package/build/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +36 -0
  80. package/{src → build/src}/app-gocardless/banks/util/escape-regexp.js +1 -1
  81. package/{src → build/src}/app-gocardless/banks/util/extract-payeeName-from-remittanceInfo.js +11 -16
  82. package/build/src/app-gocardless/banks/virgin_nrnbgb22.js +31 -0
  83. package/build/src/app-gocardless/errors.js +67 -0
  84. package/build/src/app-gocardless/gocardless-node.types.js +1 -0
  85. package/build/src/app-gocardless/gocardless.types.js +1 -0
  86. package/build/src/app-gocardless/services/gocardless-service.js +504 -0
  87. package/build/src/app-gocardless/services/tests/fixtures.js +165 -0
  88. package/build/src/app-gocardless/services/tests/gocardless-service.spec.js +387 -0
  89. package/build/src/app-gocardless/tests/bank-factory.spec.js +13 -0
  90. package/build/src/app-gocardless/tests/utils.spec.js +158 -0
  91. package/build/src/app-gocardless/util/handle-error.js +15 -0
  92. package/build/src/app-gocardless/utils.js +41 -0
  93. package/build/src/app-openid.js +83 -0
  94. package/build/src/app-pluggyai/app-pluggyai.js +164 -0
  95. package/build/src/app-pluggyai/pluggyai-service.js +97 -0
  96. package/build/src/app-secrets.js +48 -0
  97. package/build/src/app-simplefin/app-simplefin.js +335 -0
  98. package/build/src/app-sync/errors.js +12 -0
  99. package/build/src/app-sync/services/files-service.js +158 -0
  100. package/build/src/app-sync/tests/services/files-service.test.js +192 -0
  101. package/build/src/app-sync/validation.js +65 -0
  102. package/build/src/app-sync.js +302 -0
  103. package/build/src/app-sync.test.js +655 -0
  104. package/build/src/app.js +138 -0
  105. package/build/src/config-types.js +1 -0
  106. package/build/src/db.js +50 -0
  107. package/build/src/load-config.js +274 -0
  108. package/build/src/migrations.js +23 -0
  109. package/build/src/scripts/disable-openid.js +31 -0
  110. package/build/src/scripts/enable-openid.js +36 -0
  111. package/build/src/scripts/health-check.js +16 -0
  112. package/build/src/scripts/reset-password.js +40 -0
  113. package/build/src/scripts/run-migrations.js +6 -0
  114. package/build/src/secrets.test.js +68 -0
  115. package/build/src/services/secrets-service.js +79 -0
  116. package/build/src/services/user-service.js +201 -0
  117. package/build/src/sync-simple.js +68 -0
  118. package/{src → build/src}/util/hash.js +1 -2
  119. package/build/src/util/middlewares.js +49 -0
  120. package/{src → build/src}/util/paths.js +3 -6
  121. package/build/src/util/payee-name.js +37 -0
  122. package/build/src/util/prompt.js +70 -0
  123. package/build/src/util/title/index.js +43 -0
  124. package/build/src/util/title/lower-case.js +90 -0
  125. package/build/src/util/title/specials.js +21 -0
  126. package/build/src/util/validate-user.js +55 -0
  127. package/package.json +32 -36
  128. package/bin/actual-server.js +0 -117
  129. package/migrations/1694360000000-create-folders.js +0 -25
  130. package/migrations/1702667624000-rename-nordigen-secrets.js +0 -19
  131. package/src/account-db.js +0 -239
  132. package/src/accounts/openid.js +0 -368
  133. package/src/accounts/password.js +0 -149
  134. package/src/app-account.js +0 -155
  135. package/src/app-admin.js +0 -410
  136. package/src/app-admin.test.js +0 -381
  137. package/src/app-gocardless/app-gocardless.js +0 -274
  138. package/src/app-gocardless/bank-factory.js +0 -91
  139. package/src/app-gocardless/banks/abanca_caglesmm.js +0 -22
  140. package/src/app-gocardless/banks/abnamro_abnanl2a.js +0 -57
  141. package/src/app-gocardless/banks/american_express_aesudef1.js +0 -40
  142. package/src/app-gocardless/banks/bancsabadell_bsabesbbb.js +0 -31
  143. package/src/app-gocardless/banks/bank.interface.ts +0 -51
  144. package/src/app-gocardless/banks/bank_of_ireland_b365_bofiie2d.js +0 -39
  145. package/src/app-gocardless/banks/bankinter_bkbkesmm.js +0 -24
  146. package/src/app-gocardless/banks/belfius_gkccbebb.js +0 -17
  147. package/src/app-gocardless/banks/berliner_sparkasse_beladebexxx.js +0 -61
  148. package/src/app-gocardless/banks/bnp_be_gebabebb.js +0 -73
  149. package/src/app-gocardless/banks/cbc_cregbebb.js +0 -34
  150. package/src/app-gocardless/banks/commerzbank_cobadeff.js +0 -54
  151. package/src/app-gocardless/banks/danskebank_dabno22.js +0 -39
  152. package/src/app-gocardless/banks/direkt_heladef1822.js +0 -18
  153. package/src/app-gocardless/banks/easybank_bawaatww.js +0 -50
  154. package/src/app-gocardless/banks/entercard_swednokk.js +0 -40
  155. package/src/app-gocardless/banks/fortuneo_ftnofrp1xxx.js +0 -46
  156. package/src/app-gocardless/banks/hype_hyeeit22.js +0 -74
  157. package/src/app-gocardless/banks/ing_ingbrobu.js +0 -70
  158. package/src/app-gocardless/banks/ing_ingddeff.js +0 -47
  159. package/src/app-gocardless/banks/ing_pl_ingbplpw.js +0 -46
  160. package/src/app-gocardless/banks/integration-bank.js +0 -115
  161. package/src/app-gocardless/banks/isybank_itbbitmm.js +0 -18
  162. package/src/app-gocardless/banks/kbc_kredbebb.js +0 -33
  163. package/src/app-gocardless/banks/lhv-lhvbee22.js +0 -36
  164. package/src/app-gocardless/banks/mbank_retail_brexplpw.js +0 -56
  165. package/src/app-gocardless/banks/nationwide_naiagb21.js +0 -46
  166. package/src/app-gocardless/banks/nbg_ethngraaxxx.js +0 -51
  167. package/src/app-gocardless/banks/norwegian_xx_norwnok1.js +0 -74
  168. package/src/app-gocardless/banks/revolut_revolt21.js +0 -37
  169. package/src/app-gocardless/banks/sandboxfinance_sfin0000.js +0 -28
  170. package/src/app-gocardless/banks/seb_kort_bank_ab.js +0 -59
  171. package/src/app-gocardless/banks/seb_privat.js +0 -29
  172. package/src/app-gocardless/banks/sparnord_spnodk22.js +0 -24
  173. package/src/app-gocardless/banks/spk_karlsruhe_karsde66.js +0 -61
  174. package/src/app-gocardless/banks/spk_marburg_biedenkopf_heladef1mar.js +0 -30
  175. package/src/app-gocardless/banks/spk_worms_alzey_ried_malade51wor.js +0 -19
  176. package/src/app-gocardless/banks/ssk_dusseldorf_dussdeddxxx.js +0 -50
  177. package/src/app-gocardless/banks/swedbank_habalv22.js +0 -47
  178. package/src/app-gocardless/banks/tests/abanca_caglesmm.spec.js +0 -21
  179. package/src/app-gocardless/banks/tests/abnamro_abnanl2a.spec.js +0 -61
  180. package/src/app-gocardless/banks/tests/bancsabadell_bsabesbbb.spec.js +0 -53
  181. package/src/app-gocardless/banks/tests/belfius_gkccbebb.spec.js +0 -22
  182. package/src/app-gocardless/banks/tests/cbc_cregbebb.spec.js +0 -34
  183. package/src/app-gocardless/banks/tests/commerzbank_cobadeff.spec.js +0 -133
  184. package/src/app-gocardless/banks/tests/easybank_bawaatww.spec.js +0 -54
  185. package/src/app-gocardless/banks/tests/fortuneo_ftnofrp1xxx.spec.js +0 -206
  186. package/src/app-gocardless/banks/tests/ing_ingddeff.spec.js +0 -302
  187. package/src/app-gocardless/banks/tests/ing_pl_ingbplpw.spec.js +0 -202
  188. package/src/app-gocardless/banks/tests/integration_bank.spec.js +0 -156
  189. package/src/app-gocardless/banks/tests/kbc_kredbebb.spec.js +0 -38
  190. package/src/app-gocardless/banks/tests/lhv-lhvbee22.spec.js +0 -68
  191. package/src/app-gocardless/banks/tests/mbank_retail_brexplpw.spec.js +0 -171
  192. package/src/app-gocardless/banks/tests/nationwide_naiagb21.spec.js +0 -105
  193. package/src/app-gocardless/banks/tests/nbg_ethngraaxxx.spec.js +0 -48
  194. package/src/app-gocardless/banks/tests/revolut_revolt21.spec.js +0 -42
  195. package/src/app-gocardless/banks/tests/sandboxfinance_sfin0000.spec.js +0 -133
  196. package/src/app-gocardless/banks/tests/spk_marburg_biedenkopf_heladef1mar.spec.js +0 -255
  197. package/src/app-gocardless/banks/tests/ssk_dusseldorf_dussdeddxxx.spec.js +0 -100
  198. package/src/app-gocardless/banks/tests/swedbank_habalv22.spec.js +0 -57
  199. package/src/app-gocardless/banks/tests/virgin_nrnbgb22.spec.js +0 -54
  200. package/src/app-gocardless/banks/virgin_nrnbgb22.js +0 -39
  201. package/src/app-gocardless/errors.js +0 -84
  202. package/src/app-gocardless/gocardless-node.types.ts +0 -497
  203. package/src/app-gocardless/gocardless.types.ts +0 -93
  204. package/src/app-gocardless/link.html +0 -18
  205. package/src/app-gocardless/services/gocardless-service.js +0 -620
  206. package/src/app-gocardless/services/tests/fixtures.js +0 -181
  207. package/src/app-gocardless/services/tests/gocardless-service.spec.js +0 -537
  208. package/src/app-gocardless/tests/bank-factory.spec.js +0 -20
  209. package/src/app-gocardless/tests/utils.spec.js +0 -162
  210. package/src/app-gocardless/util/handle-error.js +0 -16
  211. package/src/app-gocardless/utils.js +0 -45
  212. package/src/app-openid.js +0 -108
  213. package/src/app-pluggyai/app-pluggyai.js +0 -215
  214. package/src/app-pluggyai/pluggyai-service.js +0 -120
  215. package/src/app-secrets.js +0 -61
  216. package/src/app-simplefin/app-simplefin.js +0 -405
  217. package/src/app-sync/errors.js +0 -13
  218. package/src/app-sync/services/files-service.js +0 -243
  219. package/src/app-sync/tests/services/files-service.test.js +0 -247
  220. package/src/app-sync/validation.js +0 -77
  221. package/src/app-sync.js +0 -391
  222. package/src/app-sync.test.js +0 -877
  223. package/src/app.js +0 -149
  224. package/src/config-types.ts +0 -44
  225. package/src/db.js +0 -58
  226. package/src/load-config.js +0 -307
  227. package/src/migrations.js +0 -36
  228. package/src/run-migrations.js +0 -8
  229. package/src/scripts/disable-openid.js +0 -44
  230. package/src/scripts/enable-openid.js +0 -53
  231. package/src/scripts/health-check.js +0 -23
  232. package/src/scripts/reset-password.js +0 -51
  233. package/src/secrets.test.js +0 -83
  234. package/src/services/secrets-service.js +0 -94
  235. package/src/services/user-service.js +0 -272
  236. package/src/sync-simple.js +0 -95
  237. package/src/util/middlewares.js +0 -62
  238. package/src/util/payee-name.js +0 -45
  239. package/src/util/prompt.js +0 -88
  240. package/src/util/title/index.js +0 -59
  241. package/src/util/title/lower-case.js +0 -93
  242. package/src/util/title/specials.js +0 -21
  243. package/src/util/validate-user.js +0 -68
  244. /package/{src → build/src}/sql/messages.sql +0 -0
@@ -0,0 +1,655 @@
1
+ // @ts-strict-ignore
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import { SyncProtoBuf } from '@actual-app/crdt';
5
+ import request from 'supertest';
6
+ import { getAccountDb } from './account-db.js';
7
+ import { handlers as app } from './app-sync.js';
8
+ import { getPathForUserFile } from './util/paths.js';
9
+ const ADMIN_ROLE = 'ADMIN';
10
+ const createUser = (userId, userName, role, owner = 0, enabled = 1) => {
11
+ getAccountDb().mutate('INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', [userId, userName, `${userName} display`, enabled, owner, role]);
12
+ };
13
+ describe('/user-get-key', () => {
14
+ it('returns 401 if the user is not authenticated', async () => {
15
+ const res = await request(app).post('/user-get-key');
16
+ expect(res.statusCode).toEqual(401);
17
+ expect(res.body).toEqual({
18
+ details: 'token-not-found',
19
+ reason: 'unauthorized',
20
+ status: 'error',
21
+ });
22
+ });
23
+ it('returns encryption key details for a given fileId', async () => {
24
+ const fileId = crypto.randomBytes(16).toString('hex');
25
+ const encrypt_salt = 'test-salt';
26
+ const encrypt_keyid = 'test-key-id';
27
+ const encrypt_test = 'test-encrypt-test';
28
+ getAccountDb().mutate('INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test, owner) VALUES (?, ?, ?, ?, ?)', [fileId, encrypt_salt, encrypt_keyid, encrypt_test, 'genericAdmin']);
29
+ const res = await request(app)
30
+ .post('/user-get-key')
31
+ .set('x-actual-token', 'valid-token')
32
+ .send({ fileId });
33
+ expect(res.statusCode).toEqual(200);
34
+ expect(res.body).toEqual({
35
+ status: 'ok',
36
+ data: {
37
+ id: encrypt_keyid,
38
+ salt: encrypt_salt,
39
+ test: encrypt_test,
40
+ },
41
+ });
42
+ });
43
+ it('returns 400 if the file is not found', async () => {
44
+ const res = await request(app)
45
+ .post('/user-get-key')
46
+ .set('x-actual-token', 'valid-token')
47
+ .send({ fileId: 'non-existent-file-id' });
48
+ expect(res.statusCode).toEqual(400);
49
+ expect(res.text).toBe('file-not-found');
50
+ });
51
+ });
52
+ describe('/user-create-key', () => {
53
+ it('returns 401 if the user is not authenticated', async () => {
54
+ const res = await request(app).post('/user-create-key');
55
+ expect(res.statusCode).toEqual(401);
56
+ expect(res.body).toEqual({
57
+ details: 'token-not-found',
58
+ reason: 'unauthorized',
59
+ status: 'error',
60
+ });
61
+ });
62
+ it('returns 400 if the file is not found', async () => {
63
+ const res = await request(app)
64
+ .post('/user-create-key')
65
+ .set('x-actual-token', 'valid-token')
66
+ .send({ fileId: 'non-existent-file-id' });
67
+ expect(res.statusCode).toEqual(400);
68
+ expect(res.text).toBe('file not found');
69
+ });
70
+ it('creates a new encryption key for the file', async () => {
71
+ const fileId = crypto.randomBytes(16).toString('hex');
72
+ const old_encrypt_salt = 'old-salt';
73
+ const old_encrypt_keyid = 'old-key';
74
+ const old_encrypt_test = 'old-encrypt-test';
75
+ const encrypt_salt = 'test-salt';
76
+ const encrypt_keyid = 'test-key-id';
77
+ const encrypt_test = 'test-encrypt-test';
78
+ getAccountDb().mutate('INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test) VALUES (?, ?, ?, ?)', [fileId, old_encrypt_salt, old_encrypt_keyid, old_encrypt_test]);
79
+ const res = await request(app)
80
+ .post('/user-create-key')
81
+ .set('x-actual-token', 'valid-token')
82
+ .send({
83
+ fileId,
84
+ keyId: encrypt_keyid,
85
+ keySalt: encrypt_salt,
86
+ testContent: encrypt_test,
87
+ });
88
+ expect(res.statusCode).toEqual(200);
89
+ expect(res.body).toEqual({ status: 'ok' });
90
+ const rows = getAccountDb().all('SELECT encrypt_salt, encrypt_keyid, encrypt_test FROM files WHERE id = ?', [fileId]);
91
+ expect(rows[0].encrypt_salt).toEqual(encrypt_salt);
92
+ expect(rows[0].encrypt_keyid).toEqual(encrypt_keyid);
93
+ expect(rows[0].encrypt_test).toEqual(encrypt_test);
94
+ });
95
+ });
96
+ describe('/reset-user-file', () => {
97
+ it('returns 401 if the user is not authenticated', async () => {
98
+ const res = await request(app).post('/reset-user-file');
99
+ expect(res.statusCode).toEqual(401);
100
+ expect(res.body).toEqual({
101
+ details: 'token-not-found',
102
+ reason: 'unauthorized',
103
+ status: 'error',
104
+ });
105
+ });
106
+ it('resets the user file and deletes the group file', async () => {
107
+ const fileId = crypto.randomBytes(16).toString('hex');
108
+ const groupId = 'test-group-id';
109
+ // Use addMockFile to insert a mock file into the database
110
+ getAccountDb().mutate('INSERT INTO files (id, group_id, deleted, owner) VALUES (?, ?, FALSE, ?)', [fileId, groupId, 'genericAdmin']);
111
+ getAccountDb().mutate('INSERT INTO user_access (file_id, user_id) VALUES (?, ?)', [fileId, 'genericAdmin']);
112
+ const res = await request(app)
113
+ .post('/reset-user-file')
114
+ .set('x-actual-token', 'valid-token')
115
+ .send({ fileId });
116
+ expect(res.statusCode).toEqual(200);
117
+ expect(res.body).toEqual({ status: 'ok' });
118
+ // Verify that the file is marked as deleted
119
+ const rows = getAccountDb().all('SELECT group_id FROM files WHERE id = ?', [
120
+ fileId,
121
+ ]);
122
+ expect(rows[0].group_id).toBeNull();
123
+ });
124
+ it('returns 400 if the file is not found', async () => {
125
+ const res = await request(app)
126
+ .post('/reset-user-file')
127
+ .set('x-actual-token', 'valid-token')
128
+ .send({ fileId: 'non-existent-file-id' });
129
+ expect(res.statusCode).toEqual(400);
130
+ expect(res.text).toBe('User or file not found');
131
+ });
132
+ });
133
+ describe('/upload-user-file', () => {
134
+ it('returns 401 if the user is not authenticated', async () => {
135
+ const res = await request(app).post('/upload-user-file');
136
+ expect(res.statusCode).toEqual(401);
137
+ expect(res.body).toEqual({
138
+ details: 'token-not-found',
139
+ reason: 'unauthorized',
140
+ status: 'error',
141
+ });
142
+ });
143
+ it('returns 400 if x-actual-name header is missing', async () => {
144
+ const res = await request(app)
145
+ .post('/upload-user-file')
146
+ .set('x-actual-token', 'valid-token')
147
+ .set('x-actual-file-id', 'test-file-id')
148
+ .send('file content');
149
+ expect(res.statusCode).toEqual(400);
150
+ expect(res.text).toBe('single x-actual-name is required');
151
+ });
152
+ it('returns 400 if fileId is missing', async () => {
153
+ const content = Buffer.from('file content');
154
+ const res = await request(app)
155
+ .post('/upload-user-file')
156
+ .set('Content-Type', 'application/encrypted-file')
157
+ .set('x-actual-token', 'valid-token')
158
+ .set('x-actual-name', 'test-file')
159
+ .send(content);
160
+ expect(res.statusCode).toEqual(400);
161
+ expect(res.text).toBe('fileId is required');
162
+ });
163
+ it('uploads a new file successfully', async () => {
164
+ const fileId = crypto.randomBytes(16).toString('hex');
165
+ const fileName = 'test-file.txt';
166
+ const fileContent = 'test file content';
167
+ const fileContentBuffer = Buffer.from(fileContent);
168
+ const syncVersion = 2;
169
+ const encryptMeta = JSON.stringify({ keyId: 'key-id' });
170
+ // Verify that the file does not exist before upload
171
+ const rowsBefore = getAccountDb().all('SELECT * FROM files WHERE id = ?', [
172
+ fileId,
173
+ ]);
174
+ expect(rowsBefore.length).toBe(0);
175
+ const res = await request(app)
176
+ .post('/upload-user-file')
177
+ .set('Content-Type', 'application/encrypted-file')
178
+ .set('x-actual-token', 'valid-token')
179
+ .set('x-actual-name', fileName)
180
+ .set('x-actual-file-id', fileId)
181
+ .set('x-actual-format', syncVersion.toString())
182
+ .set('x-actual-encrypt-meta', encryptMeta)
183
+ .send(fileContentBuffer);
184
+ expect(res.statusCode).toEqual(200);
185
+ expect(res.body).toEqual({ status: 'ok', groupId: expect.any(String) });
186
+ const receivedGroupid = res.body.groupId;
187
+ // Verify that the file exists in the accountDb
188
+ const rowsAfter = getAccountDb().all('SELECT * FROM files WHERE id = ?', [
189
+ fileId,
190
+ ]);
191
+ expect(rowsAfter.length).toBe(1);
192
+ expect(rowsAfter[0].id).toEqual(fileId);
193
+ expect(rowsAfter[0].group_id).toEqual(receivedGroupid);
194
+ expect(rowsAfter[0].sync_version).toEqual(syncVersion);
195
+ expect(rowsAfter[0].name).toEqual(fileName);
196
+ expect(rowsAfter[0].encrypt_meta).toEqual(encryptMeta);
197
+ // Verify that the file was written to the file system
198
+ const filePath = getPathForUserFile(fileId);
199
+ const writtenContent = await fs.promises.readFile(filePath, 'utf8');
200
+ expect(writtenContent).toEqual(fileContent);
201
+ // Clean up the file
202
+ await fs.promises.unlink(filePath);
203
+ });
204
+ it('uploads and updates an existing file successfully', async () => {
205
+ const fileId = crypto.randomBytes(16).toString('hex');
206
+ const oldGroupId = null; //sync state was reset
207
+ const oldFileName = 'old-test-file.txt';
208
+ const newFileName = 'new-test-file.txt';
209
+ const oldFileContent = 'old file content';
210
+ const newFileContent = 'new file content';
211
+ const oldSyncVersion = 1;
212
+ const newSyncVersion = 2;
213
+ const oldKeyId = 'old-key-id';
214
+ const oldEncryptMeta = JSON.stringify({ keyId: oldKeyId });
215
+ const newEncryptMeta = JSON.stringify({
216
+ keyId: oldKeyId,
217
+ sentinelValue: 1,
218
+ }); //keep the same key, but change other things
219
+ // Create the old file version
220
+ getAccountDb().mutate('INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_keyid) VALUES (?, ?, ?, ?, ?, ?)', [
221
+ fileId,
222
+ oldGroupId,
223
+ oldSyncVersion,
224
+ oldFileName,
225
+ oldEncryptMeta,
226
+ oldKeyId,
227
+ ]);
228
+ await fs.writeFile(getPathForUserFile(fileId), oldFileContent, err => {
229
+ if (err)
230
+ throw err;
231
+ });
232
+ const res = await request(app)
233
+ .post('/upload-user-file')
234
+ .set('Content-Type', 'application/encrypted-file')
235
+ .set('x-actual-token', 'valid-token')
236
+ .set('x-actual-file-id', fileId)
237
+ .set('x-actual-name', newFileName)
238
+ .set('x-actual-format', newSyncVersion.toString())
239
+ .set('x-actual-encrypt-meta', newEncryptMeta)
240
+ .send(Buffer.from(newFileContent));
241
+ expect(res.statusCode).toEqual(200);
242
+ expect(res.body).toEqual({ status: 'ok', groupId: expect.any(String) });
243
+ const receivedGroupid = res.body.groupId;
244
+ // Verify that the file was updated in the accountDb
245
+ const rowsAfter = getAccountDb().all('SELECT * FROM files WHERE id = ?', [
246
+ fileId,
247
+ ]);
248
+ expect(rowsAfter.length).toBe(1);
249
+ expect(rowsAfter[0].id).toEqual(fileId);
250
+ expect(rowsAfter[0].group_id).toEqual(receivedGroupid);
251
+ expect(rowsAfter[0].sync_version).toEqual(newSyncVersion);
252
+ expect(rowsAfter[0].name).toEqual(newFileName);
253
+ expect(rowsAfter[0].encrypt_meta).toEqual(newEncryptMeta);
254
+ // Verify that the file was written to the file system
255
+ const filePath = getPathForUserFile(fileId);
256
+ const writtenContent = await fs.promises.readFile(filePath, 'utf8');
257
+ expect(writtenContent).toEqual(newFileContent);
258
+ // Clean up the file
259
+ await fs.promises.unlink(filePath);
260
+ });
261
+ it('returns 400 if the file is part of an old group', async () => {
262
+ const fileId = crypto.randomBytes(16).toString('hex');
263
+ const groupId = 'old-group-id';
264
+ const fileName = 'test-file.txt';
265
+ const keyId = 'key-id';
266
+ const syncVersion = 2;
267
+ // Add a mock file with the old group ID
268
+ addMockFile(fileId, 'current-group-id', keyId, JSON.stringify({ keyId }), syncVersion);
269
+ const res = await request(app)
270
+ .post('/upload-user-file')
271
+ .set('Content-Type', 'application/encrypted-file')
272
+ .set('x-actual-token', 'valid-token')
273
+ .set('x-actual-file-id', fileId)
274
+ .set('x-actual-group-id', groupId)
275
+ .set('x-actual-name', fileName);
276
+ expect(res.statusCode).toEqual(400);
277
+ expect(res.text).toEqual('file-has-reset');
278
+ });
279
+ it('returns 400 if the file has a new encryption key', async () => {
280
+ const fileId = crypto.randomBytes(16).toString('hex');
281
+ const groupId = 'group-id';
282
+ const fileName = 'test-file.txt';
283
+ const oldKeyId = 'old-key-id';
284
+ const newKeyId = 'new-key-id';
285
+ const syncVersion = 2;
286
+ // Add a mock file with the new key
287
+ addMockFile(fileId, groupId, newKeyId, JSON.stringify({ newKeyId }), syncVersion);
288
+ const res = await request(app)
289
+ .post('/upload-user-file')
290
+ .set('Content-Type', 'application/encrypted-file')
291
+ .set('x-actual-token', 'valid-token')
292
+ .set('x-actual-file-id', fileId)
293
+ .set('x-actual-group-id', groupId)
294
+ .set('x-actual-name', fileName)
295
+ .set('x-actual-encrypt-meta', JSON.stringify({ keyId: oldKeyId }));
296
+ expect(res.statusCode).toEqual(400);
297
+ expect(res.text).toEqual('file-has-new-key');
298
+ });
299
+ });
300
+ describe('/download-user-file', () => {
301
+ describe('default version', () => {
302
+ it('returns 401 if the user is not authenticated', async () => {
303
+ const res = await request(app).get('/download-user-file');
304
+ expect(res.statusCode).toEqual(401);
305
+ expect(res.body).toEqual({
306
+ details: 'token-not-found',
307
+ reason: 'unauthorized',
308
+ status: 'error',
309
+ });
310
+ });
311
+ it('returns 401 if the user is invalid', async () => {
312
+ const res = await request(app)
313
+ .get('/download-user-file')
314
+ .set('x-actual-token', 'invalid-token');
315
+ expect(res.statusCode).toEqual(401);
316
+ expect(res.body).toEqual({
317
+ details: 'token-not-found',
318
+ reason: 'unauthorized',
319
+ status: 'error',
320
+ });
321
+ });
322
+ it('returns 400 error if the file does not exist in the database', async () => {
323
+ const res = await request(app)
324
+ .get('/download-user-file')
325
+ .set('x-actual-token', 'valid-token')
326
+ .set('x-actual-file-id', 'non-existing-file-id');
327
+ expect(res.statusCode).toEqual(400);
328
+ expect(res.text).toBe('User or file not found');
329
+ });
330
+ it('returns 500 error if the file does not exist on the filesystem', async () => {
331
+ getAccountDb().mutate('INSERT INTO files (id, deleted) VALUES (?, FALSE)', ['missing-fs-file']);
332
+ const res = await request(app)
333
+ .get('/download-user-file')
334
+ .set('x-actual-token', 'valid-token')
335
+ .set('x-actual-file-id', 'missing-fs-file');
336
+ expect(res.statusCode).toEqual(404);
337
+ });
338
+ it('returns an attachment file', async () => {
339
+ const fileContent = 'content';
340
+ fs.writeFileSync(getPathForUserFile('file-id'), fileContent);
341
+ getAccountDb().mutate('INSERT INTO files (id, deleted) VALUES (?, FALSE)', ['file-id']);
342
+ const res = await request(app)
343
+ .get('/download-user-file')
344
+ .set('x-actual-token', 'valid-token')
345
+ .set('x-actual-file-id', 'file-id');
346
+ expect(res.statusCode).toEqual(200);
347
+ expect(res.headers).toEqual(expect.objectContaining({
348
+ 'content-disposition': 'attachment;filename=file-id',
349
+ 'content-type': 'application/octet-stream',
350
+ }));
351
+ expect(res.body).toBeInstanceOf(Buffer);
352
+ expect(res.body.toString('utf8')).toEqual(fileContent);
353
+ });
354
+ });
355
+ });
356
+ describe('/update-user-filename', () => {
357
+ it('returns 401 if the user is not authenticated', async () => {
358
+ const res = await request(app).post('/update-user-filename');
359
+ expect(res.statusCode).toEqual(401);
360
+ expect(res.body).toEqual({
361
+ details: 'token-not-found',
362
+ reason: 'unauthorized',
363
+ status: 'error',
364
+ });
365
+ });
366
+ it('returns 400 if the file is not found', async () => {
367
+ const res = await request(app)
368
+ .post('/update-user-filename')
369
+ .set('x-actual-token', 'valid-token')
370
+ .send({ fileId: 'non-existent-file-id', name: 'new-filename' });
371
+ expect(res.statusCode).toEqual(400);
372
+ expect(res.text).toBe('file not found');
373
+ });
374
+ it('successfully updates the filename', async () => {
375
+ const fileId = crypto.randomBytes(16).toString('hex');
376
+ const oldName = 'old-filename';
377
+ const newName = 'new-filename';
378
+ // Insert a mock file into the database
379
+ getAccountDb().mutate('INSERT INTO files (id, name, deleted) VALUES (?, ?, FALSE)', [fileId, oldName]);
380
+ const res = await request(app)
381
+ .post('/update-user-filename')
382
+ .set('x-actual-token', 'valid-token')
383
+ .send({ fileId, name: newName });
384
+ expect(res.statusCode).toEqual(200);
385
+ expect(res.body).toEqual({ status: 'ok' });
386
+ // Verify that the filename was updated
387
+ const rows = getAccountDb().all('SELECT name FROM files WHERE id = ?', [
388
+ fileId,
389
+ ]);
390
+ expect(rows[0].name).toEqual(newName);
391
+ });
392
+ });
393
+ describe('/list-user-files', () => {
394
+ it('returns 401 if the user is not authenticated', async () => {
395
+ const res = await request(app).get('/list-user-files');
396
+ expect(res.statusCode).toEqual(401);
397
+ expect(res.body).toEqual({
398
+ details: 'token-not-found',
399
+ reason: 'unauthorized',
400
+ status: 'error',
401
+ });
402
+ });
403
+ it('returns a list of user files for an authenticated user', async () => {
404
+ createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1);
405
+ const fileId1 = crypto.randomBytes(16).toString('hex');
406
+ const fileId2 = crypto.randomBytes(16).toString('hex');
407
+ const fileName1 = 'file1.txt';
408
+ const fileName2 = 'file2.txt';
409
+ // Insert mock files into the database
410
+ getAccountDb().mutate('INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)', [fileId1, fileName1, '']);
411
+ getAccountDb().mutate('INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)', [fileId2, fileName2, '']);
412
+ const res = await request(app)
413
+ .get('/list-user-files')
414
+ .set('x-actual-token', 'valid-token');
415
+ expect(res.statusCode).toEqual(200);
416
+ expect(res.body).toEqual(expect.objectContaining({
417
+ status: 'ok',
418
+ data: expect.arrayContaining([
419
+ expect.objectContaining({
420
+ deleted: 0,
421
+ fileId: fileId1,
422
+ groupId: null,
423
+ name: fileName1,
424
+ encryptKeyId: null,
425
+ }),
426
+ expect.objectContaining({
427
+ deleted: 0,
428
+ fileId: fileId2,
429
+ groupId: null,
430
+ name: fileName2,
431
+ encryptKeyId: null,
432
+ }),
433
+ ]),
434
+ }));
435
+ });
436
+ });
437
+ describe('/get-user-file-info', () => {
438
+ it('returns file info for a valid fileId', async () => {
439
+ const fileId = crypto.randomBytes(16).toString('hex');
440
+ const groupId = 'test-group-id';
441
+ const fileInfo = {
442
+ id: fileId,
443
+ group_id: groupId,
444
+ name: 'test-file',
445
+ encrypt_meta: JSON.stringify({ key: 'value' }),
446
+ deleted: 0,
447
+ };
448
+ getAccountDb().mutate('INSERT INTO files (id, group_id, name, encrypt_meta, deleted) VALUES (?, ?, ?, ?, ?)', [
449
+ fileInfo.id,
450
+ fileInfo.group_id,
451
+ fileInfo.name,
452
+ fileInfo.encrypt_meta,
453
+ fileInfo.deleted,
454
+ ]);
455
+ const res = await request(app)
456
+ .get('/get-user-file-info')
457
+ .set('x-actual-token', 'valid-token')
458
+ .set('x-actual-file-id', fileId)
459
+ .send();
460
+ expect(res.statusCode).toEqual(200);
461
+ expect(res.body).toEqual({
462
+ status: 'ok',
463
+ data: {
464
+ deleted: fileInfo.deleted,
465
+ fileId: fileInfo.id,
466
+ groupId: fileInfo.group_id,
467
+ name: fileInfo.name,
468
+ encryptMeta: { key: 'value' },
469
+ usersWithAccess: [],
470
+ },
471
+ });
472
+ });
473
+ it('returns error if the file is not found', async () => {
474
+ const fileId = 'non-existent-file-id';
475
+ const res = await request(app)
476
+ .get('/get-user-file-info')
477
+ .set('x-actual-token', 'valid-token')
478
+ .set('x-actual-file-id', fileId);
479
+ expect(res.statusCode).toEqual(400);
480
+ expect(res.body).toEqual({ status: 'error', reason: 'file-not-found' });
481
+ });
482
+ it('returns error if the user is not authenticated', async () => {
483
+ // Simulate an unauthenticated request by not setting the necessary headers
484
+ const res = await request(app).get('/get-user-file-info');
485
+ expect(res.statusCode).toEqual(401);
486
+ expect(res.body).toEqual({
487
+ status: 'error',
488
+ reason: 'unauthorized',
489
+ details: 'token-not-found',
490
+ });
491
+ });
492
+ });
493
+ describe('/delete-user-file', () => {
494
+ it('returns 401 if the user is not authenticated', async () => {
495
+ const res = await request(app).post('/delete-user-file');
496
+ expect(res.statusCode).toEqual(401);
497
+ expect(res.body).toEqual({
498
+ details: 'token-not-found',
499
+ reason: 'unauthorized',
500
+ status: 'error',
501
+ });
502
+ });
503
+ // it returns 422 if the fileId is not provided
504
+ it('returns 422 if the fileId is not provided', async () => {
505
+ const res = await request(app)
506
+ .post('/delete-user-file')
507
+ .set('x-actual-token', 'valid-token');
508
+ expect(res.statusCode).toEqual(422);
509
+ expect(res.body).toEqual({
510
+ details: 'fileId-required',
511
+ reason: 'unprocessable-entity',
512
+ status: 'error',
513
+ });
514
+ });
515
+ it('returns 400 if the file does not exist', async () => {
516
+ const res = await request(app)
517
+ .post('/delete-user-file')
518
+ .set('x-actual-token', 'valid-token')
519
+ .send({ fileId: 'non-existing-file-id' });
520
+ expect(res.statusCode).toEqual(400);
521
+ expect(res.text).toEqual('file-not-found');
522
+ });
523
+ it('marks the file as deleted', async () => {
524
+ const accountDb = getAccountDb();
525
+ const fileId = crypto.randomBytes(16).toString('hex');
526
+ // Insert a file into the database
527
+ accountDb.mutate('INSERT OR IGNORE INTO files (id, deleted) VALUES (?, FALSE)', [fileId]);
528
+ const res = await request(app)
529
+ .post('/delete-user-file')
530
+ .set('x-actual-token', 'valid-token')
531
+ .send({ fileId });
532
+ expect(res.statusCode).toEqual(200);
533
+ expect(res.body).toEqual({ status: 'ok' });
534
+ // Verify that the file is marked as deleted
535
+ const rows = accountDb.all('SELECT deleted FROM files WHERE id = ?', [
536
+ fileId,
537
+ ]);
538
+ expect(rows[0].deleted).toBe(1);
539
+ });
540
+ });
541
+ describe('/sync', () => {
542
+ it('returns 401 if the user is not authenticated', async () => {
543
+ const res = await request(app).post('/sync');
544
+ expect(res.statusCode).toEqual(401);
545
+ expect(res.body).toEqual({
546
+ details: 'token-not-found',
547
+ reason: 'unauthorized',
548
+ status: 'error',
549
+ });
550
+ });
551
+ it('returns 200 and syncs successfully with correct file attributes', async () => {
552
+ const fileId = crypto.randomBytes(16).toString('hex');
553
+ const groupId = 'group-id';
554
+ const keyId = 'key-id';
555
+ const syncVersion = 2;
556
+ const encryptMeta = JSON.stringify({ keyId });
557
+ addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion);
558
+ const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
559
+ const res = await sendSyncRequest(syncRequest);
560
+ expect(res.statusCode).toEqual(200);
561
+ expect(res.headers['content-type']).toEqual('application/actual-sync');
562
+ expect(res.headers['x-actual-sync-method']).toEqual('simple');
563
+ });
564
+ it('returns 500 if the request body is invalid', async () => {
565
+ const res = await request(app)
566
+ .post('/sync')
567
+ .set('x-actual-token', 'valid-token')
568
+ // Content-Type is set correctly, but the body cannot be deserialized
569
+ .set('Content-Type', 'application/actual-sync')
570
+ .send('invalid-body');
571
+ expect(res.statusCode).toEqual(500);
572
+ expect(res.body).toEqual({
573
+ status: 'error',
574
+ reason: 'internal-error',
575
+ });
576
+ });
577
+ it('returns 422 if since is not provided', async () => {
578
+ const syncRequest = createMinimalSyncRequest('file-id', 'group-id', 'key-id');
579
+ syncRequest.setSince(undefined);
580
+ const res = await sendSyncRequest(syncRequest);
581
+ expect(res.statusCode).toEqual(422);
582
+ expect(res.body).toEqual({
583
+ status: 'error',
584
+ reason: 'unprocessable-entity',
585
+ details: 'since-required',
586
+ });
587
+ });
588
+ it('returns 400 if the file does not exist in the database', async () => {
589
+ const syncRequest = createMinimalSyncRequest('non-existant-file-id', 'group-id', 'key-id');
590
+ // We do not insert the file into the database, so it does not exist
591
+ const res = await sendSyncRequest(syncRequest);
592
+ expect(res.statusCode).toEqual(400);
593
+ expect(res.text).toEqual('file-not-found');
594
+ });
595
+ it('returns 400 if the file sync version is old', async () => {
596
+ const fileId = crypto.randomBytes(16).toString('hex');
597
+ const groupId = 'group-id';
598
+ const keyId = 'key-id';
599
+ const oldSyncVersion = 1; // Assuming SYNC_FORMAT_VERSION is 2
600
+ // Add a mock file with an old sync version
601
+ addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), oldSyncVersion);
602
+ const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
603
+ const res = await sendSyncRequest(syncRequest);
604
+ expect(res.statusCode).toEqual(400);
605
+ expect(res.text).toEqual('file-old-version');
606
+ });
607
+ it('returns 400 if the file needs to be uploaded (no group_id)', async () => {
608
+ const fileId = crypto.randomBytes(16).toString('hex');
609
+ const groupId = null; // No group ID
610
+ const keyId = 'key-id';
611
+ const syncVersion = 2;
612
+ addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion);
613
+ const syncRequest = createMinimalSyncRequest(fileId, groupId, keyId);
614
+ const res = await sendSyncRequest(syncRequest);
615
+ expect(res.statusCode).toEqual(400);
616
+ expect(res.text).toEqual('file-needs-upload');
617
+ });
618
+ it('returns 400 if the file has a new encryption key', async () => {
619
+ const fileId = crypto.randomBytes(16).toString('hex');
620
+ const groupId = 'group-id';
621
+ const keyId = 'old-key-id';
622
+ const newKeyId = 'new-key-id';
623
+ const syncVersion = 2;
624
+ // Add a mock file with the old key
625
+ addMockFile(fileId, groupId, keyId, JSON.stringify({ keyId }), syncVersion);
626
+ // Create a sync request with the new key
627
+ const syncRequest = createMinimalSyncRequest(fileId, groupId, newKeyId);
628
+ const res = await sendSyncRequest(syncRequest);
629
+ expect(res.statusCode).toEqual(400);
630
+ expect(res.text).toEqual('file-has-new-key');
631
+ });
632
+ });
633
+ function addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion) {
634
+ getAccountDb().mutate('INSERT INTO files (id, group_id, encrypt_keyid, encrypt_meta, sync_version, owner) VALUES (?, ?, ?,?, ?, ?)', [fileId, groupId, keyId, encryptMeta, syncVersion, 'genericAdmin']);
635
+ }
636
+ function createMinimalSyncRequest(fileId, groupId, keyId) {
637
+ const syncRequest = new SyncProtoBuf.SyncRequest();
638
+ syncRequest.setFileid(fileId);
639
+ syncRequest.setGroupid(groupId);
640
+ syncRequest.setKeyid(keyId);
641
+ syncRequest.setSince('2024-01-01T00:00:00.000Z');
642
+ syncRequest.setMessagesList([]);
643
+ return syncRequest;
644
+ }
645
+ async function sendSyncRequest(syncRequest) {
646
+ const serializedRequest = syncRequest.serializeBinary();
647
+ // Convert Uint8Array to Buffer
648
+ const bufferRequest = Buffer.from(serializedRequest);
649
+ const res = await request(app)
650
+ .post('/sync')
651
+ .set('x-actual-token', 'valid-token')
652
+ .set('Content-Type', 'application/actual-sync')
653
+ .send(bufferRequest);
654
+ return res;
655
+ }