settings-store.e2e-spec.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. import {
  2. CurrencyCode,
  3. LanguageCode,
  4. mergeConfig,
  5. Permission,
  6. SettingsStoreEntry,
  7. SettingsStoreService,
  8. TransactionalConnection,
  9. } from '@vendure/core';
  10. import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
  11. import path from 'path';
  12. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  13. import { initialData } from '../../../e2e-common/e2e-initial-data';
  14. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  15. import { SettingsStoreTestPlugin } from './fixtures/test-plugins/settings-store-test-plugin';
  16. import {
  17. createAdministratorDocument,
  18. createChannelDocument,
  19. createRoleDocument,
  20. getSettingsStoreValueDocument,
  21. getSettingsStoreValuesDocument,
  22. setSettingsStoreValueDocument,
  23. setSettingsStoreValuesDocument,
  24. } from './graphql/shared-definitions';
  25. describe('SettingsStore system', () => {
  26. const { server, adminClient } = createTestEnvironment(
  27. mergeConfig(testConfig(), {
  28. plugins: [SettingsStoreTestPlugin],
  29. }),
  30. );
  31. beforeAll(async () => {
  32. await server.init({
  33. initialData,
  34. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  35. customerCount: 1,
  36. });
  37. await adminClient.asSuperAdmin();
  38. }, TEST_SETUP_TIMEOUT_MS);
  39. afterAll(async () => {
  40. await server.destroy();
  41. });
  42. describe('Global scoped fields', () => {
  43. it('should set and get a global value', async () => {
  44. const { setSettingsStoreValue } = await adminClient.query(setSettingsStoreValueDocument, {
  45. input: { key: 'test.globalSetting', value: 'global-value' },
  46. });
  47. expect(setSettingsStoreValue.result).toBe(true);
  48. expect(setSettingsStoreValue.key).toBe('test.globalSetting');
  49. expect(setSettingsStoreValue.error).toBeNull();
  50. const { getSettingsStoreValue } = await adminClient.query(getSettingsStoreValueDocument, {
  51. key: 'test.globalSetting',
  52. });
  53. expect(getSettingsStoreValue).toBe('global-value');
  54. });
  55. it('should return same global value from different contexts', async () => {
  56. // Create another user
  57. await adminClient.query(createAdministratorDocument, {
  58. input: {
  59. firstName: 'Test',
  60. lastName: 'Admin',
  61. emailAddress: 'test@test.com',
  62. password: 'password',
  63. roleIds: ['1'], // SuperAdmin role
  64. },
  65. });
  66. // Login as the new user and check global value
  67. await adminClient.asUserWithCredentials('test@test.com', 'password');
  68. const { getSettingsStoreValue } = await adminClient.query(getSettingsStoreValueDocument, {
  69. key: 'test.globalSetting',
  70. });
  71. expect(getSettingsStoreValue).toBe('global-value');
  72. });
  73. });
  74. describe('User scoped fields', () => {
  75. beforeAll(async () => {
  76. await adminClient.asSuperAdmin();
  77. });
  78. it('should store separate values per user', async () => {
  79. // Set value as superadmin
  80. await adminClient.query(setSettingsStoreValueDocument, {
  81. input: { key: 'test.userSetting', value: 'superadmin-value' },
  82. });
  83. // Create and switch to another user
  84. await adminClient.query(createAdministratorDocument, {
  85. input: {
  86. firstName: 'Test2',
  87. lastName: 'Admin2',
  88. emailAddress: 'test2@test.com',
  89. password: 'password',
  90. roleIds: ['1'],
  91. },
  92. });
  93. await adminClient.asUserWithCredentials('test2@test.com', 'password');
  94. // Should not see superadmin's value
  95. const { getSettingsStoreValue: emptyValue } = await adminClient.query(
  96. getSettingsStoreValueDocument,
  97. {
  98. key: 'test.userSetting',
  99. },
  100. );
  101. expect(emptyValue).toBeNull();
  102. // Set different value for this user
  103. await adminClient.query(setSettingsStoreValueDocument, {
  104. input: { key: 'test.userSetting', value: 'test2-value' },
  105. });
  106. const { getSettingsStoreValue: userValue } = await adminClient.query(
  107. getSettingsStoreValueDocument,
  108. {
  109. key: 'test.userSetting',
  110. },
  111. );
  112. expect(userValue).toBe('test2-value');
  113. // Switch back to superadmin and verify original value
  114. await adminClient.asSuperAdmin();
  115. const { getSettingsStoreValue: superadminValue } = await adminClient.query(
  116. getSettingsStoreValueDocument,
  117. {
  118. key: 'test.userSetting',
  119. },
  120. );
  121. expect(superadminValue).toBe('superadmin-value');
  122. });
  123. });
  124. describe('Channel scoped fields', () => {
  125. const testChannelToken = 'test-channel-token';
  126. it('should store separate values per channel', async () => {
  127. await adminClient.asSuperAdmin();
  128. // Set value in default channel
  129. await adminClient.query(setSettingsStoreValueDocument, {
  130. input: { key: 'test.channelSetting', value: 'default-channel-value' },
  131. });
  132. const defaultZoneId = 'T_1';
  133. // Create a new channel
  134. await adminClient.query(createChannelDocument, {
  135. input: {
  136. code: 'test-channel',
  137. token: testChannelToken,
  138. defaultLanguageCode: LanguageCode.en,
  139. currencyCode: CurrencyCode.USD,
  140. pricesIncludeTax: false,
  141. defaultShippingZoneId: defaultZoneId,
  142. defaultTaxZoneId: defaultZoneId,
  143. },
  144. });
  145. // Switch to new channel
  146. adminClient.setChannelToken(testChannelToken);
  147. // Should not see default channel's value
  148. const { getSettingsStoreValue: emptyValue } = await adminClient.query(
  149. getSettingsStoreValueDocument,
  150. {
  151. key: 'test.channelSetting',
  152. },
  153. );
  154. expect(emptyValue).toBeNull();
  155. // Set different value for this channel
  156. await adminClient.query(setSettingsStoreValueDocument, {
  157. input: { key: 'test.channelSetting', value: 'test-channel-value' },
  158. });
  159. const { getSettingsStoreValue: channelValue } = await adminClient.query(
  160. getSettingsStoreValueDocument,
  161. {
  162. key: 'test.channelSetting',
  163. },
  164. );
  165. expect(channelValue).toBe('test-channel-value');
  166. // Switch back to default channel and verify original value
  167. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  168. const { getSettingsStoreValue: defaultValue } = await adminClient.query(
  169. getSettingsStoreValueDocument,
  170. {
  171. key: 'test.channelSetting',
  172. },
  173. );
  174. expect(defaultValue).toBe('default-channel-value');
  175. });
  176. });
  177. describe('User and channel scoped fields', () => {
  178. const testChannelToken = 'test-channel-token';
  179. it('should store separate values per user per channel', async () => {
  180. await adminClient.asSuperAdmin();
  181. // Set value as superadmin in default channel
  182. await adminClient.query(setSettingsStoreValueDocument, {
  183. input: { key: 'test.userAndChannelSetting', value: 'superadmin-default' },
  184. });
  185. // Switch to test channel
  186. adminClient.setChannelToken(testChannelToken);
  187. // Should not see default channel value
  188. const { getSettingsStoreValue: emptyValue } = await adminClient.query(
  189. getSettingsStoreValueDocument,
  190. {
  191. key: 'test.userAndChannelSetting',
  192. },
  193. );
  194. expect(emptyValue).toBeNull();
  195. // Set different value for superadmin in test channel
  196. await adminClient.query(setSettingsStoreValueDocument, {
  197. input: { key: 'test.userAndChannelSetting', value: 'superadmin-test' },
  198. });
  199. // Switch to test2 user in test channel
  200. await adminClient.asUserWithCredentials('test2@test.com', 'password');
  201. adminClient.setChannelToken(testChannelToken);
  202. // Should not see superadmin's value
  203. const { getSettingsStoreValue: emptyUserValue } = await adminClient.query(
  204. getSettingsStoreValueDocument,
  205. {
  206. key: 'test.userAndChannelSetting',
  207. },
  208. );
  209. expect(emptyUserValue).toBeNull();
  210. // Set value for test2 user in test channel
  211. await adminClient.query(setSettingsStoreValueDocument, {
  212. input: { key: 'test.userAndChannelSetting', value: 'test2-test' },
  213. });
  214. // Verify all combinations maintain separate values
  215. const { getSettingsStoreValue: test2TestValue } = await adminClient.query(
  216. getSettingsStoreValueDocument,
  217. {
  218. key: 'test.userAndChannelSetting',
  219. },
  220. );
  221. expect(test2TestValue).toBe('test2-test');
  222. // Switch back to superadmin in test channel
  223. await adminClient.asSuperAdmin();
  224. adminClient.setChannelToken(testChannelToken);
  225. const { getSettingsStoreValue: superadminTestValue } = await adminClient.query(
  226. getSettingsStoreValueDocument,
  227. {
  228. key: 'test.userAndChannelSetting',
  229. },
  230. );
  231. expect(superadminTestValue).toBe('superadmin-test');
  232. // Switch to default channel
  233. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  234. const { getSettingsStoreValue: superadminDefaultValue } = await adminClient.query(
  235. getSettingsStoreValueDocument,
  236. {
  237. key: 'test.userAndChannelSetting',
  238. },
  239. );
  240. expect(superadminDefaultValue).toBe('superadmin-default');
  241. });
  242. });
  243. describe('Bulk operations', () => {
  244. it('should get multiple values', async () => {
  245. await adminClient.asSuperAdmin();
  246. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  247. const result = await adminClient.query(getSettingsStoreValuesDocument, {
  248. keys: ['test.globalSetting', 'test.userSetting'],
  249. });
  250. expect(result.getSettingsStoreValues).toEqual({
  251. 'test.globalSetting': 'global-value',
  252. 'test.userSetting': 'superadmin-value',
  253. });
  254. });
  255. it('should set multiple values', async () => {
  256. await adminClient.asSuperAdmin();
  257. const { setSettingsStoreValues } = await adminClient.query(setSettingsStoreValuesDocument, {
  258. inputs: [
  259. { key: 'test.bulk1', value: 'bulk-value-1' },
  260. { key: 'test.bulk2', value: 'bulk-value-2' },
  261. ],
  262. });
  263. expect(setSettingsStoreValues).toHaveLength(2);
  264. expect(setSettingsStoreValues[0].result).toBe(true);
  265. expect(setSettingsStoreValues[0].key).toBe('test.bulk1');
  266. expect(setSettingsStoreValues[1].result).toBe(true);
  267. expect(setSettingsStoreValues[1].key).toBe('test.bulk2');
  268. const result = await adminClient.query(getSettingsStoreValuesDocument, {
  269. keys: ['test.bulk1', 'test.bulk2'],
  270. });
  271. expect(result.getSettingsStoreValues).toEqual({
  272. 'test.bulk1': 'bulk-value-1',
  273. 'test.bulk2': 'bulk-value-2',
  274. });
  275. });
  276. });
  277. describe('Complex data types', () => {
  278. it('should handle JSON objects', async () => {
  279. await adminClient.asSuperAdmin();
  280. const complexData = {
  281. nested: {
  282. array: [1, 2, 3],
  283. boolean: true,
  284. null: null,
  285. },
  286. string: 'test',
  287. };
  288. await adminClient.query(setSettingsStoreValueDocument, {
  289. input: { key: 'test.complexData', value: complexData },
  290. });
  291. const { getSettingsStoreValue } = await adminClient.query(getSettingsStoreValueDocument, {
  292. key: 'test.complexData',
  293. });
  294. expect(getSettingsStoreValue).toEqual(complexData);
  295. });
  296. });
  297. describe('Validation', () => {
  298. it('should validate values according to field config', async () => {
  299. await adminClient.asSuperAdmin();
  300. // Try to set invalid theme value - should return structured error result
  301. const invalidResult = await adminClient.query(setSettingsStoreValueDocument, {
  302. input: { key: 'test.validatedField', value: 'invalid-value' },
  303. });
  304. expect(invalidResult.setSettingsStoreValue.result).toBe(false);
  305. expect(invalidResult.setSettingsStoreValue.key).toBe('test.validatedField');
  306. expect(invalidResult.setSettingsStoreValue.error).toContain('Validation failed');
  307. // Set valid value should work
  308. const { setSettingsStoreValue } = await adminClient.query(setSettingsStoreValueDocument, {
  309. input: { key: 'test.validatedField', value: 'valid-option' },
  310. });
  311. expect(setSettingsStoreValue.result).toBe(true);
  312. expect(setSettingsStoreValue.error).toBeNull();
  313. });
  314. });
  315. describe('Readonly fields', () => {
  316. it('should prevent modification of readonly fields', async () => {
  317. await adminClient.asSuperAdmin();
  318. const { setSettingsStoreValue } = await adminClient.query(setSettingsStoreValueDocument, {
  319. input: { key: 'test.readonlyField', value: 'attempt-change' },
  320. });
  321. expect(setSettingsStoreValue.result).toBe(false);
  322. expect(setSettingsStoreValue.key).toBe('test.readonlyField');
  323. expect(setSettingsStoreValue.error).toContain('readonly');
  324. });
  325. });
  326. describe('Permission handling', () => {
  327. it('should reject users without required permissions', async () => {
  328. await adminClient.asSuperAdmin();
  329. // Create a role with limited permissions (no CreateAdministrator permission)
  330. const { createRole } = await adminClient.query(createRoleDocument, {
  331. input: {
  332. code: 'limited-role',
  333. description: 'Limited permissions role',
  334. permissions: [Permission.Authenticated, Permission.ReadAdministrator], // No CreateAdministrator
  335. },
  336. });
  337. // Create a user with limited permissions
  338. await adminClient.query(createAdministratorDocument, {
  339. input: {
  340. firstName: 'Limited',
  341. lastName: 'User',
  342. emailAddress: 'limited@test.com',
  343. password: 'password',
  344. roleIds: [createRole.id],
  345. },
  346. });
  347. // Switch to limited user
  348. await adminClient.asUserWithCredentials('limited@test.com', 'password');
  349. // Try to access admin-only field - should get null (no access)
  350. const { getSettingsStoreValue: deniedValue } = await adminClient.query(
  351. getSettingsStoreValueDocument,
  352. {
  353. key: 'test.adminOnlyField',
  354. },
  355. );
  356. expect(deniedValue).toBeNull();
  357. // Try to set admin-only field - should return structured error result
  358. const { setSettingsStoreValue } = await adminClient.query(setSettingsStoreValueDocument, {
  359. input: { key: 'test.adminOnlyField', value: 'denied-value' },
  360. });
  361. expect(setSettingsStoreValue.result).toBe(false);
  362. expect(setSettingsStoreValue.key).toBe('test.adminOnlyField');
  363. expect(setSettingsStoreValue.error).toContain('Insufficient permissions');
  364. });
  365. it('should allow users with required permissions', async () => {
  366. await adminClient.asSuperAdmin();
  367. // SuperAdmin should have all permissions
  368. const { setSettingsStoreValue } = await adminClient.query(setSettingsStoreValueDocument, {
  369. input: { key: 'test.adminOnlyField', value: 'admin-value' },
  370. });
  371. expect(setSettingsStoreValue.result).toBe(true);
  372. expect(setSettingsStoreValue.key).toBe('test.adminOnlyField');
  373. expect(setSettingsStoreValue.error).toBeNull();
  374. const { getSettingsStoreValue } = await adminClient.query(getSettingsStoreValueDocument, {
  375. key: 'test.adminOnlyField',
  376. });
  377. expect(getSettingsStoreValue).toBe('admin-value');
  378. });
  379. });
  380. describe('Invalid key handling', () => {
  381. it('should gracefully handle getting invalid keys', async () => {
  382. await adminClient.asSuperAdmin();
  383. try {
  384. await adminClient.query(getSettingsStoreValueDocument, {
  385. key: 'invalid.nonExistentKey',
  386. });
  387. expect.fail('Should have thrown an error for invalid key');
  388. } catch (error) {
  389. expect((error as Error).message).toContain('not registered');
  390. }
  391. });
  392. it('should gracefully handle setting invalid keys', async () => {
  393. await adminClient.asSuperAdmin();
  394. const { setSettingsStoreValue } = await adminClient.query(setSettingsStoreValueDocument, {
  395. input: { key: 'invalid.nonExistentKey', value: 'some-value' },
  396. });
  397. expect(setSettingsStoreValue.result).toBe(false);
  398. expect(setSettingsStoreValue.key).toBe('invalid.nonExistentKey');
  399. expect(setSettingsStoreValue.error).toContain('not registered');
  400. });
  401. });
  402. describe('Bulk operations with mixed keys', () => {
  403. it('should handle bulk get with one valid, one invalid key', async () => {
  404. await adminClient.asSuperAdmin();
  405. try {
  406. await adminClient.query(getSettingsStoreValuesDocument, {
  407. keys: ['test.globalSetting', 'invalid.nonExistentKey'],
  408. });
  409. expect.fail('Should have thrown an error for invalid key in bulk operation');
  410. } catch (error) {
  411. expect((error as Error).message).toContain('not registered');
  412. }
  413. });
  414. it('should handle bulk set with one valid, one invalid key', async () => {
  415. await adminClient.asSuperAdmin();
  416. const { setSettingsStoreValues } = await adminClient.query(setSettingsStoreValuesDocument, {
  417. inputs: [
  418. { key: 'test.bulk1', value: 'valid-value' },
  419. { key: 'invalid.nonExistentKey', value: 'invalid-value' },
  420. ],
  421. });
  422. expect(setSettingsStoreValues).toHaveLength(2);
  423. expect(setSettingsStoreValues[0].result).toBe(true);
  424. expect(setSettingsStoreValues[0].key).toBe('test.bulk1');
  425. expect(setSettingsStoreValues[0].error).toBeNull();
  426. expect(setSettingsStoreValues[1].result).toBe(false);
  427. expect(setSettingsStoreValues[1].key).toBe('invalid.nonExistentKey');
  428. expect(setSettingsStoreValues[1].error).toContain('not registered');
  429. });
  430. it('should handle bulk operations with permission-restricted keys', async () => {
  431. await adminClient.asSuperAdmin();
  432. // First set a value as admin
  433. await adminClient.query(setSettingsStoreValueDocument, {
  434. input: { key: 'test.adminOnlyField', value: 'admin-bulk-value' },
  435. });
  436. // Switch to limited user
  437. await adminClient.asUserWithCredentials('limited@test.com', 'password');
  438. // Try bulk get with mix of accessible and restricted keys
  439. const { getSettingsStoreValues } = await adminClient.query(getSettingsStoreValuesDocument, {
  440. keys: ['test.globalSetting', 'test.adminOnlyField'],
  441. });
  442. // Should only return values for accessible keys
  443. expect(getSettingsStoreValues).toEqual({
  444. 'test.globalSetting': 'global-value',
  445. // adminOnlyField should be omitted due to permissions
  446. });
  447. // Try bulk set with mix of accessible and restricted keys
  448. const { setSettingsStoreValues } = await adminClient.query(setSettingsStoreValuesDocument, {
  449. inputs: [
  450. { key: 'test.globalSetting', value: 'new-global-value' },
  451. { key: 'test.adminOnlyField', value: 'denied-value' },
  452. ],
  453. });
  454. expect(setSettingsStoreValues).toHaveLength(2);
  455. expect(setSettingsStoreValues[0].result).toBe(true);
  456. expect(setSettingsStoreValues[0].key).toBe('test.globalSetting');
  457. expect(setSettingsStoreValues[0].error).toBeNull();
  458. expect(setSettingsStoreValues[1].result).toBe(false);
  459. expect(setSettingsStoreValues[1].key).toBe('test.adminOnlyField');
  460. expect(setSettingsStoreValues[1].error).toContain('Insufficient permissions');
  461. });
  462. });
  463. describe('Orphaned entries cleanup', () => {
  464. let settingsStoreService: SettingsStoreService;
  465. beforeAll(async () => {
  466. await adminClient.asSuperAdmin();
  467. // Get the SettingsStoreService directly from the application
  468. try {
  469. settingsStoreService = server.app.get(SettingsStoreService);
  470. } catch (error) {
  471. // eslint-disable-next-line no-console
  472. console.error('Failed to get SettingsStoreService:', error);
  473. // Try getting it from the service module
  474. settingsStoreService = server.app.get('SettingsStoreService');
  475. }
  476. });
  477. it('should identify orphaned entries', async () => {
  478. // Insert some orphaned entries directly into the database
  479. const connection = server.app.get(TransactionalConnection);
  480. const repo = connection.rawConnection.getRepository(SettingsStoreEntry);
  481. // Create entries with keys that don't have field definitions
  482. await repo.save([
  483. {
  484. key: 'orphaned.oldSetting1',
  485. value: 'old-value-1',
  486. scope: '',
  487. updatedAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000), // 8 days ago
  488. },
  489. {
  490. key: 'orphaned.oldSetting2',
  491. value: { complex: 'object', array: [1, 2, 3] },
  492. scope: 'user:123',
  493. updatedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10 days ago
  494. },
  495. {
  496. key: 'orphaned.recentSetting',
  497. value: 'recent-value',
  498. scope: '',
  499. updatedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // 1 day ago
  500. },
  501. ]);
  502. // Test finding orphaned entries older than 7 days
  503. const orphanedEntries = await settingsStoreService.findOrphanedEntries({
  504. olderThan: '7d',
  505. maxDeleteCount: 100,
  506. });
  507. expect(orphanedEntries.length).toBe(2); // Should find the 8-day and 10-day old entries
  508. expect(orphanedEntries.map(e => e.key)).toEqual(
  509. expect.arrayContaining(['orphaned.oldSetting1', 'orphaned.oldSetting2']),
  510. );
  511. expect(orphanedEntries.find(e => e.key === 'orphaned.oldSetting1')?.scope).toBe('');
  512. expect(orphanedEntries.find(e => e.key === 'orphaned.oldSetting2')?.scope).toBe('user:123');
  513. expect(orphanedEntries.find(e => e.key === 'orphaned.oldSetting2')?.valuePreview).toContain(
  514. 'complex',
  515. );
  516. });
  517. it('should perform dry-run cleanup', async () => {
  518. const result = await settingsStoreService.cleanupOrphanedEntries({
  519. olderThan: '7d',
  520. dryRun: true,
  521. maxDeleteCount: 100,
  522. });
  523. expect(result.dryRun).toBe(true);
  524. expect(result.deletedCount).toBe(2); // Should find 2 entries to delete
  525. expect(result.deletedEntries.length).toBeLessThanOrEqual(10); // Sample entries
  526. // Verify entries are still in database (dry run shouldn't delete)
  527. const connection = server.app.get(TransactionalConnection);
  528. const repo = connection.rawConnection.getRepository(SettingsStoreEntry);
  529. const remainingEntries = await repo.find({
  530. where: [{ key: 'orphaned.oldSetting1' }, { key: 'orphaned.oldSetting2' }],
  531. });
  532. expect(remainingEntries.length).toBe(2);
  533. });
  534. it('should actually cleanup orphaned entries', async () => {
  535. const result = await settingsStoreService.cleanupOrphanedEntries({
  536. olderThan: '7d',
  537. dryRun: false,
  538. maxDeleteCount: 100,
  539. batchSize: 50,
  540. });
  541. expect(result.dryRun).toBe(false);
  542. expect(result.deletedCount).toBe(2);
  543. expect(result.deletedEntries.length).toBeLessThanOrEqual(10);
  544. // Verify entries are actually deleted from database
  545. const connection = server.app.get(TransactionalConnection);
  546. const repo = connection.rawConnection.getRepository(SettingsStoreEntry);
  547. const remainingEntries = await repo.find({
  548. where: [{ key: 'orphaned.oldSetting1' }, { key: 'orphaned.oldSetting2' }],
  549. });
  550. expect(remainingEntries.length).toBe(0);
  551. // Recent entry should still exist (not old enough)
  552. const recentEntry = await repo.findOne({ where: { key: 'orphaned.recentSetting' } });
  553. expect(recentEntry).not.toBeNull();
  554. });
  555. it('should respect age thresholds with ms package formats', async () => {
  556. // Create entries of different ages
  557. const connection = server.app.get(TransactionalConnection);
  558. const repo = connection.rawConnection.getRepository(SettingsStoreEntry);
  559. await repo.save([
  560. {
  561. key: 'orphaned.veryOld',
  562. value: 'very-old',
  563. scope: '',
  564. updatedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
  565. },
  566. {
  567. key: 'orphaned.somewhartOld',
  568. value: 'somewhat-old',
  569. scope: '',
  570. updatedAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // 5 days ago
  571. },
  572. ]);
  573. // Test different ms package formats
  574. const old10d = await settingsStoreService.findOrphanedEntries({ olderThan: '10d' });
  575. expect(old10d.map(e => e.key)).toContain('orphaned.veryOld');
  576. expect(old10d.map(e => e.key)).not.toContain('orphaned.somewhartOld');
  577. const old3d = await settingsStoreService.findOrphanedEntries({ olderThan: '3 days' });
  578. expect(old3d.map(e => e.key)).toContain('orphaned.veryOld');
  579. expect(old3d.map(e => e.key)).toContain('orphaned.somewhartOld');
  580. const old1w = await settingsStoreService.findOrphanedEntries({ olderThan: '1w' });
  581. expect(old1w.map(e => e.key)).toContain('orphaned.veryOld');
  582. expect(old1w.map(e => e.key)).not.toContain('orphaned.somewhartOld');
  583. // Cleanup
  584. await settingsStoreService.cleanupOrphanedEntries({ olderThan: '1d' });
  585. });
  586. });
  587. });