settings-store.e2e-spec.ts 34 KB

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