passbolt-browser-extension 5.12.1 → 5.13.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 (198) hide show
  1. package/.devcontainer/safe-chain-config.json +2 -1
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +30 -22
  3. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. package/.jpmignore +0 -1
  5. package/CHANGELOG.md +82 -1
  6. package/CONTRIBUTING.md +1 -12
  7. package/README.md +5 -16
  8. package/RELEASE_NOTES.md +2 -3
  9. package/SECURITY.md +7 -0
  10. package/i18next.config.js +28 -0
  11. package/jest.config.json +1 -0
  12. package/package.json +20 -40
  13. package/src/all/background_page/controller/accountRecovery/reviewRequestController.test.js +1 -1
  14. package/src/all/background_page/{event → controller/actionLog}/findAllForActionLogController.js +1 -1
  15. package/src/all/background_page/{event → controller/actionLog}/findAllForActionLogController.test.js +4 -4
  16. package/src/all/background_page/controller/export/exportResourcesFileController.test.js +7 -2
  17. package/src/all/background_page/controller/folder/folderCreateController.js +3 -1
  18. package/src/all/background_page/controller/group/findMyGroupsController.test.js +2 -2
  19. package/src/all/background_page/controller/group/getOrFindGroupsController.js +61 -0
  20. package/src/all/background_page/controller/group/getOrFindGroupsController.test.js +69 -0
  21. package/src/all/background_page/controller/group/getOrFindGroupsUsersController.js +62 -0
  22. package/src/all/background_page/controller/group/getOrFindGroupsUsersController.test.js +69 -0
  23. package/src/all/background_page/controller/group/groupCreateController.js +1 -1
  24. package/src/all/background_page/controller/group/groupCreateController.test.js +1 -1
  25. package/src/all/background_page/controller/group/groupUpdateController.js +1 -1
  26. package/src/all/background_page/controller/group/updateAllGroupsLocalStorageController.test.js +1 -1
  27. package/src/all/background_page/controller/keyring/synchroniseKeyringController.js +51 -0
  28. package/src/all/background_page/controller/keyring/synchroniseKeyringController.test.js +49 -0
  29. package/src/all/background_page/controller/metadata/shareMetadataKeyPrivateController.test.js +1 -1
  30. package/src/all/background_page/controller/move/moveFolderController.js +0 -2
  31. package/src/all/background_page/controller/permission/FindAcoPermissionsForDisplayController.js +1 -1
  32. package/src/all/background_page/controller/permission/FindAcoPermissionsForDisplayController.test.js +2 -2
  33. package/src/all/background_page/controller/resource/findAllByIdsForDisplayPermissionsController.test.js +8 -3
  34. package/src/all/background_page/controller/resource/findAllIdsByIsSharedWithGroupController.test.js +9 -4
  35. package/src/all/background_page/controller/resource/resourceUpdateController.test.js +1 -2
  36. package/src/all/background_page/controller/resourceLocalStorage/resourceUpdateLocalStorageController.test.js +5 -2
  37. package/src/all/background_page/controller/share/findFoldersForShareController.js +66 -0
  38. package/src/all/background_page/controller/share/findFoldersForShareController.test.js +70 -0
  39. package/src/all/background_page/controller/share/searchUsersAndGroupsController.js +4 -4
  40. package/src/all/background_page/controller/share/searchUsersAndGroupsController.test.js +8 -23
  41. package/src/all/background_page/controller/share/shareResourcesController.test.js +2 -2
  42. package/src/all/background_page/controller/subscription/createSubscriptionKeyController.js +63 -0
  43. package/src/all/background_page/controller/subscription/createSubscriptionKeyController.test.js +56 -0
  44. package/src/all/background_page/controller/subscription/deleteSubscriptionKeyController.js +55 -0
  45. package/src/all/background_page/controller/subscription/deleteSubscriptionKeyController.test.js +50 -0
  46. package/src/all/background_page/controller/user/deleteUserController.test.js +1 -1
  47. package/src/all/background_page/controller/user/getOrFindUsersController.js +61 -0
  48. package/src/all/background_page/controller/user/getOrFindUsersController.test.js +69 -0
  49. package/src/all/background_page/error/deleteDryRunError.js +1 -1
  50. package/src/all/background_page/event/actionLogEvents.js +1 -1
  51. package/src/all/background_page/event/appEvents.js +25 -0
  52. package/src/all/background_page/event/groupEvents.js +26 -0
  53. package/src/all/background_page/event/keyringEvents.js +12 -0
  54. package/src/all/background_page/event/shareEvents.js +3 -9
  55. package/src/all/background_page/event/userEvents.js +13 -0
  56. package/src/all/background_page/model/config.js +12 -2
  57. package/src/all/background_page/model/entity/folder/folderEntity.js +2 -2
  58. package/src/all/background_page/model/entity/folder/folderEntity.test.js +2 -2
  59. package/src/all/background_page/model/entity/folder/foldersCollection.test.js +1 -1
  60. package/src/all/background_page/model/entity/group/update/groupUpdateEntity.js +1 -1
  61. package/src/all/background_page/model/entity/group/update/groupUpdateEntity.test.js +1 -1
  62. package/src/all/background_page/model/entity/permission/actionLog/updatedPermissionEntity.js +2 -2
  63. package/src/all/background_page/model/entity/permission/actionLog/updatedPermissionEntity.test.data.js +1 -1
  64. package/src/all/background_page/model/entity/permission/change/permissionChangeEntity.js +1 -1
  65. package/src/all/background_page/model/entity/permission/change/permissionChangesCollection.js +2 -2
  66. package/src/all/background_page/model/entity/permission/change/permissionChangesCollection.test.js +2 -2
  67. package/src/all/background_page/model/entity/resource/resourceEntity.js +2 -2
  68. package/src/all/background_page/model/entity/resource/resourceEntity.test.js +1 -1
  69. package/src/all/background_page/model/entity/user/userEntity.js +16 -377
  70. package/src/all/background_page/model/entity/user/userEntity.test.js +22 -297
  71. package/src/all/background_page/model/entity/userAndGroupSearchResultEntity/userAndGroupSearchResultEntity.js +1 -1
  72. package/src/all/background_page/model/folder/folderModel.js +5 -154
  73. package/src/all/background_page/model/group/groupModel.js +1 -1
  74. package/src/all/background_page/model/keyring.js +52 -17
  75. package/src/all/background_page/model/keyring.test.js +110 -0
  76. package/src/all/background_page/model/resource/resourceModel.js +2 -55
  77. package/src/all/background_page/model/setup/setupModel.js +2 -2
  78. package/src/all/background_page/model/user/userModel.js +18 -15
  79. package/src/all/background_page/model/user/userModel.test.js +1 -3
  80. package/src/all/background_page/service/api/abstract/abstractService.js +3 -17
  81. package/src/all/background_page/service/api/edition/passboltEditionApiService.js +64 -0
  82. package/src/all/background_page/service/api/edition/passboltEditionApiService.test.js +99 -0
  83. package/src/all/background_page/service/api/group/groupApiService.js +22 -23
  84. package/src/all/background_page/service/api/group/groupApiService.test.js +70 -0
  85. package/src/all/background_page/service/api/resource/resourceService.js +18 -12
  86. package/src/all/background_page/service/api/share/{shareService.js → shareApiService.js} +10 -7
  87. package/src/all/background_page/service/api/share/{shareService.test.js → shareApiService.test.js} +5 -5
  88. package/src/all/background_page/service/group/createGroupService.js +1 -1
  89. package/src/all/background_page/service/group/createGroupService.test.js +1 -1
  90. package/src/all/background_page/service/group/findAndUpdateGroupsLocalStorageService.js +56 -1
  91. package/src/all/background_page/service/group/findAndUpdateGroupsLocalStorageService.test.js +84 -2
  92. package/src/all/background_page/service/group/findGroupsService.js +5 -9
  93. package/src/all/background_page/service/group/findGroupsService.test.data.js +1 -1
  94. package/src/all/background_page/service/group/findGroupsService.test.js +10 -15
  95. package/src/all/background_page/service/group/getOrFindGroupsService.js +65 -0
  96. package/src/all/background_page/service/group/getOrFindGroupsService.test.js +168 -0
  97. package/src/all/background_page/service/group/getOrFindGroupsUsersService.js +51 -0
  98. package/src/all/background_page/service/group/getOrFindGroupsUsersService.test.js +94 -0
  99. package/src/all/background_page/service/group/groupUpdateService.js +5 -3
  100. package/src/all/background_page/service/group/groupUpdateService.test.js +10 -2
  101. package/src/all/background_page/service/local_storage/groupLocalStorage.js +2 -2
  102. package/src/all/background_page/service/local_storage/groupLocalStorage.test.js +3 -3
  103. package/src/all/background_page/service/local_storage/userLocalStorage.js +57 -36
  104. package/src/all/background_page/service/local_storage/userLocalStorage.test.js +282 -0
  105. package/src/all/background_page/service/metadata/createMetadataKeyService.test.js +1 -1
  106. package/src/all/background_page/service/metadata/saveMetadataSettingsService.test.js +1 -1
  107. package/src/all/background_page/service/metadata/shareMetadataKeyPrivateService.test.js +1 -1
  108. package/src/all/background_page/service/migrateMetadata/migrateMetadataResourcesService.js +1 -1
  109. package/src/all/background_page/service/move/calculatePermissionsChangesForMoveService.js +113 -0
  110. package/src/all/background_page/service/move/calculatePermissionsChangesForMoveService.test.data.js +38 -0
  111. package/src/all/background_page/service/move/calculatePermissionsChangesForMoveService.test.js +158 -0
  112. package/src/all/background_page/service/move/moveOneFolderService.js +6 -7
  113. package/src/all/background_page/service/move/moveOneFolderService.test.js +90 -90
  114. package/src/all/background_page/service/move/moveResourcesService.js +2 -5
  115. package/src/all/background_page/service/permission/findPermissionsService.js +1 -1
  116. package/src/all/background_page/service/resource/create/resourceCreateService.js +13 -31
  117. package/src/all/background_page/service/resource/create/resourceCreateService.test.js +25 -18
  118. package/src/all/background_page/service/resource/export/exportResourcesService.test.js +13 -4
  119. package/src/all/background_page/service/resource/findAndUpdateResourcesLocalStorageService.test.js +35 -28
  120. package/src/all/background_page/service/resource/findResourcesService.js +78 -2
  121. package/src/all/background_page/service/resource/findResourcesService.test.data.js +1 -1
  122. package/src/all/background_page/service/resource/findResourcesService.test.js +90 -31
  123. package/src/all/background_page/service/resource/getOrFindResourcesService.test.js +18 -8
  124. package/src/all/background_page/service/session_storage/keepSessionAliveService.js +3 -3
  125. package/src/all/background_page/service/session_storage/keepSessionAliveService.test.js +5 -3
  126. package/src/all/background_page/service/share/searchUsersAndGroupsService.js +41 -0
  127. package/src/all/background_page/service/share/searchUsersAndGroupsService.test.js +64 -0
  128. package/src/all/background_page/service/share/shareFoldersService.js +3 -3
  129. package/src/all/background_page/service/share/shareFoldersService.test.js +3 -3
  130. package/src/all/background_page/service/share/shareResourceService.js +4 -4
  131. package/src/all/background_page/service/share/shareResourceService.test.js +8 -8
  132. package/src/all/background_page/service/subscription/createSubscriptionKeyService.js +57 -0
  133. package/src/all/background_page/service/subscription/createSubscriptionKeyService.test.js +111 -0
  134. package/src/all/background_page/service/subscription/deleteSubscriptionKeyService.js +35 -0
  135. package/src/all/background_page/service/subscription/deleteSubscriptionKeyService.test.js +55 -0
  136. package/src/all/background_page/service/user/deleteUserService.js +4 -4
  137. package/src/all/background_page/service/user/deleteUserService.test.js +10 -10
  138. package/src/all/background_page/service/user/findAndUpdateUsersLocalStorageService.js +81 -0
  139. package/src/all/background_page/service/user/findAndUpdateUsersLocalStorageService.test.js +132 -0
  140. package/src/all/background_page/service/user/findUsersService.js +6 -6
  141. package/src/all/background_page/service/user/findUsersService.test.js +39 -38
  142. package/src/all/background_page/service/user/getOrFindUsersService.js +60 -0
  143. package/src/all/background_page/service/user/getOrFindUsersService.test.js +110 -0
  144. package/src/all/background_page/utils/assertions.js +1 -0
  145. package/src/all/locales/ko-KR/common.json +1 -1
  146. package/src/chrome/manifest.json +1 -1
  147. package/src/chrome-mv3/manifest.json +1 -1
  148. package/src/firefox/manifest.json +3 -3
  149. package/src/safari/manifest.json +2 -2
  150. package/test/jest.env-setup.js +31 -0
  151. package/webpack/applyOutputClean.js +69 -0
  152. package/webpack/base.config.js +33 -0
  153. package/webpack/common-blocks.js +91 -0
  154. package/webpack/expectedBuildArtifacts.js +51 -0
  155. package/webpack/i18nextExtractionPlugin.js +43 -0
  156. package/webpack/passboltEnvPlugin.js +41 -0
  157. package/webpack/webExtPlugin/index.js +75 -0
  158. package/webpack.chromium-mv2.config.js +40 -0
  159. package/webpack.chromium-mv3.config.js +40 -0
  160. package/webpack.common.config.js +186 -0
  161. package/webpack.config.js +38 -0
  162. package/webpack.firefox.config.js +40 -0
  163. package/webpack.mv2.config.js +65 -0
  164. package/webpack.mv3.config.js +99 -0
  165. package/webpack.safari-background-page.config.js +66 -57
  166. package/webpack.safari.config.js +44 -0
  167. package/Gruntfile.js +0 -471
  168. package/am_i_compromised.py +0 -1036
  169. package/am_i_compromised.sh +0 -688
  170. package/i18next-parser.config.js +0 -22
  171. package/src/all/background_page/config/config.json +0 -7
  172. package/src/all/background_page/config/config.json.debug +0 -7
  173. package/src/all/background_page/config/config.json.default +0 -7
  174. package/src/all/background_page/model/entity/group/groupEntity.js +0 -241
  175. package/src/all/background_page/model/entity/group/groupEntity.test.js +0 -136
  176. package/src/all/background_page/model/entity/group/groupsCollection.js +0 -166
  177. package/src/all/background_page/model/entity/group/groupsCollection.test.data.js +0 -34
  178. package/src/all/background_page/model/entity/group/groupsCollection.test.js +0 -227
  179. package/src/all/background_page/model/entity/permission/permissionEntity.js +0 -485
  180. package/src/all/background_page/model/entity/permission/permissionEntity.test.js +0 -263
  181. package/src/all/background_page/model/entity/permission/permissionsCollection.js +0 -486
  182. package/src/all/background_page/model/entity/permission/permissionsCollection.test.js +0 -700
  183. package/src/all/background_page/model/entity/user/usersCollection.js +0 -147
  184. package/src/all/background_page/model/entity/user/usersCollection.test.js +0 -223
  185. package/src/all/background_page/model/share/shareModel.js +0 -183
  186. package/src/all/background_page/model/share/shareModel.test.js +0 -61
  187. package/src/all/background_page/service/api/user/userService.js +0 -260
  188. package/src/all/background_page/service/resource/create/resourceCreateService.test.data.js +0 -55
  189. package/webpack-content-scripts.browser-integration.config.js +0 -57
  190. package/webpack-content-scripts.config.js +0 -61
  191. package/webpack-content-scripts.public-website-sign-in.config.js +0 -57
  192. package/webpack-data.config.js +0 -102
  193. package/webpack-data.download.config.js +0 -59
  194. package/webpack-data.in-form-call-to-action.config.js +0 -97
  195. package/webpack-data.in-form-menu.config.js +0 -97
  196. package/webpack-offscreens.config.js +0 -55
  197. package/webpack.background-page.config.js +0 -62
  198. package/webpack.service-worker.config.js +0 -65
@@ -12,8 +12,9 @@
12
12
  * @since 3.0.0
13
13
  */
14
14
  import AbstractService from "../abstract/abstractService";
15
+ import PassboltResponseEntity from "passbolt-styleguide/src/shared/models/entity/apiService/PassboltResponseEntity";
15
16
 
16
- const GROUP_SERVICE_RESOURCE_NAME = "groups";
17
+ const GROUP_API_SERVICE_RESOURCE_NAME = "groups";
17
18
 
18
19
  class GroupApiService extends AbstractService {
19
20
  /**
@@ -33,7 +34,7 @@ class GroupApiService extends AbstractService {
33
34
  * @public
34
35
  */
35
36
  static get RESOURCE_NAME() {
36
- return GROUP_SERVICE_RESOURCE_NAME;
37
+ return GROUP_API_SERVICE_RESOURCE_NAME;
37
38
  }
38
39
 
39
40
  /**
@@ -65,16 +66,7 @@ class GroupApiService extends AbstractService {
65
66
  * @returns {Array<string>} list of supported option
66
67
  */
67
68
  static getSupportedFiltersOptions() {
68
- return ["has-users", "has-managers"];
69
- }
70
-
71
- /**
72
- * Return the list of supported orders for in API find operations
73
- *
74
- * @returns {Array<string>} list of supported option
75
- */
76
- static getSupportedOrdersOptions() {
77
- return ["Group.name DESC", "Group.name ASC"];
69
+ return ["has-users", "has-managers", "has-id"];
78
70
  }
79
71
 
80
72
  /**
@@ -96,24 +88,28 @@ class GroupApiService extends AbstractService {
96
88
  *
97
89
  * @param {Object} [contains] optional example: {permissions: true}
98
90
  * @param {Object} [filters] optional
99
- * @param {Object} [orders] optional
100
- * @returns {Promise<*>} response body
91
+ * @returns {Promise<PassboltResponseEntity>}
101
92
  * @throws {Error} if options are invalid or API error
102
93
  * @public
103
94
  */
104
- async findAll(contains, filters, orders) {
95
+ async findAll(contains, filters) {
96
+ const hasIdFilter = filters?.["has-id"];
105
97
  const legacyContain = GroupApiService.remapLegacyContain(contains); // crassette
106
98
  contains = legacyContain
107
99
  ? this.formatContainOptions(legacyContain, GroupApiService.getSupportedContainOptions())
108
100
  : null;
109
101
  filters = filters ? this.formatFilterOptions(filters, GroupApiService.getSupportedFiltersOptions()) : null;
110
- orders = orders ? this.formatOrderOptions(orders, GroupApiService.getSupportedFiltersOptions()) : null;
111
- const options = { ...contains, ...filters, ...orders };
112
- const response = await this.apiClient.findAll(options);
113
- if (!response.body || !response.body.length) {
114
- return [];
102
+ const options = { ...contains, ...filters };
103
+ const rawResponse = await this.apiClient.findAll(options);
104
+ /*
105
+ * Ensure backward compatibility with servers that do not yet support the has-id filter:
106
+ * post-filter the results in memory so the caller always receives only the requested groups.
107
+ */
108
+ if (hasIdFilter && rawResponse.body) {
109
+ const idsMap = new Set(Array.isArray(hasIdFilter) ? hasIdFilter : [hasIdFilter]);
110
+ rawResponse.body = rawResponse.body.filter((group) => idsMap.has(group.id));
115
111
  }
116
- return response.body;
112
+ return new PassboltResponseEntity(rawResponse);
117
113
  }
118
114
 
119
115
  /**
@@ -142,10 +138,13 @@ class GroupApiService extends AbstractService {
142
138
  * @throw {TypeError} if group id is not a valid uuid
143
139
  * @public
144
140
  */
145
- async update(groupId, groupData) {
141
+ async update(groupId, groupData, contains) {
146
142
  this.assertValidId(groupId);
147
143
  this.assertNonEmptyData(groupData);
148
- const response = await this.apiClient.update(groupId, groupData);
144
+ const urlOptions = contains
145
+ ? this.formatContainOptions(contains, GroupApiService.getSupportedContainOptions())
146
+ : {};
147
+ const response = await this.apiClient.update(groupId, groupData, urlOptions);
149
148
  return response.body;
150
149
  }
151
150
 
@@ -11,7 +11,77 @@
11
11
  * @link https://www.passbolt.com Passbolt(tm)
12
12
  * @since 2.13.0
13
13
  */
14
+ import { enableFetchMocks } from "jest-fetch-mock";
15
+ import { mockApiResponse } from "../../../../../../test/mocks/mockApiResponse";
14
16
  import GroupApiService from "./groupApiService";
17
+ import { defaultApiClientOptions } from "passbolt-styleguide/src/shared/lib/apiClient/apiClientOptions.test.data";
18
+ import { defaultGroupDto } from "passbolt-styleguide/src/shared/models/entity/group/groupEntity.test.data";
19
+ import PassboltResponseEntity from "passbolt-styleguide/src/shared/models/entity/apiService/PassboltResponseEntity";
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ enableFetchMocks();
24
+ });
25
+
26
+ describe("GroupApiService", () => {
27
+ describe("::findAll", () => {
28
+ it("should return a PassboltResponseEntity containing all groups when no has-id filter is given", async () => {
29
+ expect.assertions(3);
30
+ const groupsDto = [defaultGroupDto(), defaultGroupDto()];
31
+ fetch.doMockOnceIf(/groups\.json/, () => mockApiResponse(groupsDto));
32
+
33
+ const service = new GroupApiService(defaultApiClientOptions());
34
+ const response = await service.findAll();
35
+
36
+ expect(response).toBeInstanceOf(PassboltResponseEntity);
37
+ expect(response.body).toHaveLength(2);
38
+ expect(response.body).toStrictEqual(groupsDto);
39
+ });
40
+
41
+ it("should post-filter results to only return groups matching the has-id filter", async () => {
42
+ expect.assertions(3);
43
+ const group1 = defaultGroupDto();
44
+ const group2 = defaultGroupDto();
45
+ const group3 = defaultGroupDto();
46
+ fetch.doMockOnceIf(/groups\.json/, () => mockApiResponse([group1, group2, group3]));
47
+
48
+ const service = new GroupApiService(defaultApiClientOptions());
49
+ const response = await service.findAll(null, { "has-id": [group1.id, group3.id] });
50
+
51
+ expect(response).toBeInstanceOf(PassboltResponseEntity);
52
+ expect(response.body).toHaveLength(2);
53
+ expect(response.body).toStrictEqual([group1, group3]);
54
+ });
55
+
56
+ it("should support a single string value for the has-id filter", async () => {
57
+ expect.assertions(3);
58
+ const group1 = defaultGroupDto();
59
+ const group2 = defaultGroupDto();
60
+ fetch.doMockOnceIf(/groups\.json/, () => mockApiResponse([group1, group2]));
61
+
62
+ const service = new GroupApiService(defaultApiClientOptions());
63
+ const response = await service.findAll(null, { "has-id": group2.id });
64
+
65
+ expect(response).toBeInstanceOf(PassboltResponseEntity);
66
+ expect(response.body).toHaveLength(1);
67
+ expect(response.body).toStrictEqual([group2]);
68
+ });
69
+
70
+ it("should return an empty body when no groups match the has-id filter", async () => {
71
+ expect.assertions(2);
72
+ const group1 = defaultGroupDto();
73
+ const group2 = defaultGroupDto();
74
+ fetch.doMockOnceIf(/groups\.json/, () => mockApiResponse([group1, group2]));
75
+
76
+ const unrelatedId = defaultGroupDto().id;
77
+ const service = new GroupApiService(defaultApiClientOptions());
78
+ const response = await service.findAll(null, { "has-id": [unrelatedId] });
79
+
80
+ expect(response).toBeInstanceOf(PassboltResponseEntity);
81
+ expect(response.body).toHaveLength(0);
82
+ });
83
+ });
84
+ });
15
85
 
16
86
  describe("Group entity", () => {
17
87
  it("remap legacy contains", () => {
@@ -10,7 +10,9 @@
10
10
  * @license https://opensource.org/licenses/AGPL-3.0 AGPL License
11
11
  * @link https://www.passbolt.com Passbolt(tm)
12
12
  */
13
- import AbstractService from "../abstract/abstractService";
13
+
14
+ import PassboltResponseEntity from "passbolt-styleguide/src/shared/models/entity/apiService/PassboltResponseEntity";
15
+ import AbstractService from "passbolt-styleguide/src/shared/services/api/abstract/abstractService";
14
16
 
15
17
  const RESOURCE_SERVICE_RESOURCE_NAME = "resources";
16
18
 
@@ -82,8 +84,8 @@ class ResourceService extends AbstractService {
82
84
  *
83
85
  * @returns {Array<string>} list of supported option
84
86
  */
85
- static getSupportedOrdersOptions() {
86
- return ["Resource.modified DESC", "Resource.modified ASC"];
87
+ static getSupportedSortsOptions() {
88
+ return ["Resources.modified"];
87
89
  }
88
90
 
89
91
  /**
@@ -107,21 +109,25 @@ class ResourceService extends AbstractService {
107
109
  *
108
110
  * @param {Object} [contains] optional example: {permissions: true}
109
111
  * @param {Object} [filters] optional
110
- * @param {Object} [orders] optional
112
+ * @param {Object} [pageOptions] optional
113
+ * @param {number} [pageOptions.page] optional
114
+ * @param {number} [pageOptions.limit] optional
115
+ * @param {Object} [pageOptions.sorts] optional
116
+ * @param {Array<string>} [pageOptions.orders] optional
111
117
  * @returns {Promise<*>} response body
112
118
  * @throws {Error} if options are invalid or API error
113
119
  * @public
114
120
  */
115
- async findAll(contains, filters, orders) {
121
+ async findAll(contains, filters, pageOptions) {
116
122
  contains = contains ? this.formatContainOptions(contains, ResourceService.getSupportedContainOptions()) : null;
117
123
  filters = filters ? this.formatFilterOptions(filters, ResourceService.getSupportedFiltersOptions()) : null;
118
- orders = orders ? this.formatOrderOptions(orders, ResourceService.getSupportedFiltersOptions()) : null;
119
- const options = { ...contains, ...filters, ...orders };
120
- const response = await this.apiClient.findAll(options);
121
- if (!response.body || !response.body.length) {
122
- return [];
123
- }
124
- return response.body;
124
+ pageOptions = pageOptions ? this.formatPageOptions(pageOptions, ResourceService.getSupportedSortsOptions()) : null;
125
+
126
+ const options = { ...contains, ...filters, ...pageOptions };
127
+ const responseDto = await this.apiClient.findAll(options);
128
+
129
+ const responseDtoBody = !responseDto.body || !responseDto.body.length ? [] : responseDto.body;
130
+ return new PassboltResponseEntity({ header: responseDto.header, body: responseDtoBody });
125
131
  }
126
132
 
127
133
  /**
@@ -13,9 +13,9 @@
13
13
  import { assertString } from "../../../utils/assertions";
14
14
  import AbstractService from "../abstract/abstractService";
15
15
 
16
- const SHARE_SERVICE_RESOURCE_NAME = "share";
16
+ const SHARE_API_SERVICE_RESOURCE_NAME = "share";
17
17
 
18
- class ShareService extends AbstractService {
18
+ class ShareApiService extends AbstractService {
19
19
  /**
20
20
  * Constructor
21
21
  *
@@ -23,7 +23,7 @@ class ShareService extends AbstractService {
23
23
  * @public
24
24
  */
25
25
  constructor(apiClientOptions) {
26
- super(apiClientOptions, ShareService.RESOURCE_NAME);
26
+ super(apiClientOptions, ShareApiService.RESOURCE_NAME);
27
27
  }
28
28
 
29
29
  /**
@@ -51,7 +51,7 @@ class ShareService extends AbstractService {
51
51
  * @public
52
52
  */
53
53
  static get RESOURCE_NAME() {
54
- return SHARE_SERVICE_RESOURCE_NAME;
54
+ return SHARE_API_SERVICE_RESOURCE_NAME;
55
55
  }
56
56
 
57
57
  /**
@@ -110,8 +110,11 @@ class ShareService extends AbstractService {
110
110
  */
111
111
  async searchUsersAndGroups(keyword, contains) {
112
112
  assertString(keyword, "keyword is not a valid string");
113
- const filter = this.formatFilterOptions({ search: keyword }, ShareService.getSupportedSearchArosFiltersOptions());
114
- contains = this.formatContainOptions(contains, ShareService.getSupportedSearchArosContainOptions());
113
+ const filter = this.formatFilterOptions(
114
+ { search: keyword },
115
+ ShareApiService.getSupportedSearchArosFiltersOptions(),
116
+ );
117
+ contains = this.formatContainOptions(contains, ShareApiService.getSupportedSearchArosContainOptions());
115
118
  const options = { ...filter, ...contains };
116
119
  const url = "search-aros";
117
120
  const response = await this.apiClient.get(url, options);
@@ -119,4 +122,4 @@ class ShareService extends AbstractService {
119
122
  }
120
123
  }
121
124
 
122
- export default ShareService;
125
+ export default ShareApiService;
@@ -18,7 +18,7 @@ import {
18
18
  defaultGroupSearchResultDto,
19
19
  defaultUserSearchResultDto,
20
20
  } from "../../../model/entity/userAndGroupSearchResultEntity/userAndGroupSearchResultEntity.test.data";
21
- import ShareService from "./shareService";
21
+ import ShareApiService from "./shareApiService";
22
22
  import { defaultApiClientOptions } from "passbolt-styleguide/src/shared/lib/apiClient/apiClientOptions.test.data";
23
23
 
24
24
  beforeEach(() => {
@@ -26,7 +26,7 @@ beforeEach(() => {
26
26
  enableFetchMocks();
27
27
  });
28
28
 
29
- describe("ShareService", () => {
29
+ describe("ShareApiService", () => {
30
30
  describe("::searchUsersAndGroups", () => {
31
31
  it("should return a collection dto from the API", async () => {
32
32
  expect.assertions(1);
@@ -38,7 +38,7 @@ describe("ShareService", () => {
38
38
  fetch.doMockOnceIf(/share\/search-aros\.json/, () => mockApiResponse(expectedDto));
39
39
 
40
40
  const keyword = "test";
41
- const service = new ShareService(defaultApiClientOptions());
41
+ const service = new ShareApiService(defaultApiClientOptions());
42
42
  const resultDto = await service.searchUsersAndGroups(keyword, contains);
43
43
 
44
44
  expect(resultDto).toStrictEqual(expectedDto);
@@ -61,7 +61,7 @@ describe("ShareService", () => {
61
61
  });
62
62
 
63
63
  const keyword = "test";
64
- const service = new ShareService(defaultApiClientOptions());
64
+ const service = new ShareApiService(defaultApiClientOptions());
65
65
  const resultDto = await service.searchUsersAndGroups(keyword, contains);
66
66
 
67
67
  expect(resultDto).toStrictEqual(expectedDto);
@@ -70,7 +70,7 @@ describe("ShareService", () => {
70
70
  it("should throw an excpetion if the search keyword is empty", async () => {
71
71
  expect.assertions(1);
72
72
 
73
- const service = new ShareService(defaultApiClientOptions());
73
+ const service = new ShareApiService(defaultApiClientOptions());
74
74
  try {
75
75
  await service.searchUsersAndGroups(1, {});
76
76
  } catch (e) {
@@ -14,7 +14,7 @@
14
14
 
15
15
  import GroupApiService from "../api/group/groupApiService";
16
16
  import GroupLocalStorage from "../local_storage/groupLocalStorage";
17
- import GroupEntity from "../../model/entity/group/groupEntity";
17
+ import GroupEntity from "passbolt-styleguide/src/shared/models/entity/group/groupEntity";
18
18
  import { assertType } from "../../utils/assertions";
19
19
 
20
20
  export default class CreateGroupService {
@@ -16,7 +16,7 @@ import AccountEntity from "../../model/entity/account/accountEntity";
16
16
  import { defaultAccountDto } from "../../model/entity/account/accountEntity.test.data";
17
17
  import { defaultApiClientOptions } from "passbolt-styleguide/src/shared/lib/apiClient/apiClientOptions.test.data";
18
18
  import CreateGroupService from "./createGroupService";
19
- import GroupEntity from "../../model/entity/group/groupEntity";
19
+ import GroupEntity from "passbolt-styleguide/src/shared/models/entity/group/groupEntity";
20
20
  import { defaultGroupDto } from "passbolt-styleguide/src/shared/models/entity/group/groupEntity.test.data";
21
21
 
22
22
  describe("CreateGroupService", () => {
@@ -13,7 +13,10 @@
13
13
  */
14
14
  import GroupLocalStorage from "../local_storage/groupLocalStorage";
15
15
  import FindGroupsService from "./findGroupsService";
16
- import GroupsCollection from "../../model/entity/group/groupsCollection";
16
+ import GroupApiService from "../api/group/groupApiService";
17
+ import GroupEntity from "passbolt-styleguide/src/shared/models/entity/group/groupEntity";
18
+ import GroupsCollection from "passbolt-styleguide/src/shared/models/entity/group/groupsCollection";
19
+ import { assertArrayUUID } from "../../utils/assertions";
17
20
 
18
21
  const GROUPS_UPDATE_ALL_LS_LOCK_PREFIX = "GROUPS_UPDATE_LS_LOCK_";
19
22
 
@@ -30,6 +33,7 @@ class FindAndUpdateGroupsLocalStorageService {
30
33
  this.account = account;
31
34
  this.groupLocalStorage = new GroupLocalStorage(account);
32
35
  this.findGroupsService = new FindGroupsService(apiClientOptions);
36
+ this.groupApiService = new GroupApiService(apiClientOptions);
33
37
  this.lockKey = `${GROUPS_UPDATE_ALL_LS_LOCK_PREFIX}-${this.account.id}`;
34
38
  }
35
39
 
@@ -60,6 +64,57 @@ class FindAndUpdateGroupsLocalStorageService {
60
64
  return updatedResourcesCollection;
61
65
  });
62
66
  }
67
+
68
+ /**
69
+ * Retrieve groups from local storage for the given ids.
70
+ * If any requested group is missing from the local storage, the missing ones are fetched from the API,
71
+ * added to the local storage, and included in the returned collection.
72
+ *
73
+ * @param {Array<string>} groupIds The ids of the groups to retrieve.
74
+ * @returns {Promise<GroupsCollection>}
75
+ * @public
76
+ */
77
+ async findForLocalStorageByIds(groupIds) {
78
+ assertArrayUUID(groupIds);
79
+
80
+ const groupsDtos = (await this.groupLocalStorage.get()) ?? [];
81
+ const requestedIdsSet = new Set(groupIds);
82
+ const collection = new GroupsCollection(
83
+ groupsDtos.filter((dto) => requestedIdsSet.has(dto.id)),
84
+ { validate: false },
85
+ );
86
+
87
+ if (collection.length === groupIds.length) {
88
+ return collection;
89
+ }
90
+
91
+ // Missing groups need to be fetched from the API and stored.
92
+ // Use a lock to prevent concurrent writes to the local storage.
93
+ return await navigator.locks.request(this.lockKey, async () => {
94
+ // Re-check local storage after acquiring the lock, as another concurrent
95
+ // call may have already fetched and stored the missing groups.
96
+ const refreshedGroupsDtos = (await this.groupLocalStorage.get()) ?? [];
97
+ const refreshedCollection = new GroupsCollection(
98
+ refreshedGroupsDtos.filter((dto) => requestedIdsSet.has(dto.id)),
99
+ { validate: false },
100
+ );
101
+
102
+ if (refreshedCollection.length === groupIds.length) {
103
+ return refreshedCollection;
104
+ }
105
+
106
+ const foundIdsSet = new Set(refreshedCollection.items.map((entity) => entity.id));
107
+ const missingIds = groupIds.filter((id) => !foundIdsSet.has(id));
108
+ const response = await this.groupApiService.findAll(GroupLocalStorage.DEFAULT_CONTAIN, { "has-id": missingIds });
109
+ for (const groupDto of response.body ?? []) {
110
+ const groupEntity = new GroupEntity(groupDto);
111
+ await this.groupLocalStorage.addGroup(groupEntity);
112
+ refreshedCollection.push(groupEntity);
113
+ }
114
+
115
+ return refreshedCollection;
116
+ });
117
+ }
63
118
  }
64
119
 
65
120
  export default FindAndUpdateGroupsLocalStorageService;
@@ -16,8 +16,9 @@ import AccountEntity from "../../model/entity/account/accountEntity";
16
16
  import { defaultAccountDto } from "../../model/entity/account/accountEntity.test.data";
17
17
  import { defaultApiClientOptions } from "passbolt-styleguide/src/shared/lib/apiClient/apiClientOptions.test.data";
18
18
  import FindAndUpdateGroupsLocalStorageService from "./findAndUpdateGroupsLocalStorageService";
19
- import GroupsCollection from "../../model/entity/group/groupsCollection";
20
- import { defaultGroupsDtos } from "../../model/entity/group/groupsCollection.test.data";
19
+ import GroupsCollection from "passbolt-styleguide/src/shared/models/entity/group/groupsCollection";
20
+ import { defaultGroupDto } from "passbolt-styleguide/src/shared/models/entity/group/groupEntity.test.data";
21
+ import { defaultGroupsDtos } from "passbolt-styleguide/src/shared/models/entity/group/groupsCollection.test.data";
21
22
  import FindGroupsService from "./findGroupsService";
22
23
 
23
24
  beforeEach(() => {
@@ -113,4 +114,85 @@ describe("FindAndUpdateGroupsLocalStorageService", () => {
113
114
  expect(groupsLSDto).toEqual(groupsDto);
114
115
  });
115
116
  });
117
+
118
+ describe("::findForLocalStorageByIds", () => {
119
+ it("should return only the requested groups from local storage when all ids are present", async () => {
120
+ expect.assertions(3);
121
+
122
+ const groupsDtos = defaultGroupsDtos(5);
123
+ const collection = new GroupsCollection(groupsDtos);
124
+ await findAndUpdateGroupsLocalStorageService.groupLocalStorage.set(collection);
125
+
126
+ const requestedIds = [groupsDtos[0].id, groupsDtos[2].id];
127
+ jest.spyOn(findAndUpdateGroupsLocalStorageService.groupApiService, "findAll");
128
+
129
+ const result = await findAndUpdateGroupsLocalStorageService.findForLocalStorageByIds(requestedIds);
130
+
131
+ expect(findAndUpdateGroupsLocalStorageService.groupApiService.findAll).not.toHaveBeenCalled();
132
+ expect(result).toBeInstanceOf(GroupsCollection);
133
+ expect(result.items.map((g) => g.id)).toStrictEqual(requestedIds);
134
+ });
135
+
136
+ it("should fetch missing groups from the API, add them to local storage, and return the full requested collection", async () => {
137
+ expect.assertions(5);
138
+
139
+ const storedGroupsDtos = defaultGroupsDtos(3);
140
+ const storedCollection = new GroupsCollection(storedGroupsDtos);
141
+ await findAndUpdateGroupsLocalStorageService.groupLocalStorage.set(storedCollection);
142
+
143
+ const missingGroupDto = defaultGroupDto();
144
+ jest
145
+ .spyOn(findAndUpdateGroupsLocalStorageService.groupApiService, "findAll")
146
+ .mockResolvedValue({ body: [missingGroupDto] });
147
+
148
+ const requestedIds = [storedGroupsDtos[0].id, missingGroupDto.id];
149
+ const result = await findAndUpdateGroupsLocalStorageService.findForLocalStorageByIds(requestedIds);
150
+
151
+ expect(findAndUpdateGroupsLocalStorageService.groupApiService.findAll).toHaveBeenCalledTimes(1);
152
+ expect(findAndUpdateGroupsLocalStorageService.groupApiService.findAll).toHaveBeenCalledWith(
153
+ { groups_users: true, my_group_user: true },
154
+ { "has-id": [missingGroupDto.id] },
155
+ );
156
+ expect(result).toBeInstanceOf(GroupsCollection);
157
+ expect(result.items.map((g) => g.id)).toStrictEqual(requestedIds);
158
+
159
+ const storageValue = await findAndUpdateGroupsLocalStorageService.groupLocalStorage.get();
160
+ expect(storageValue.find((dto) => dto.id === missingGroupDto.id)).toBeDefined();
161
+ });
162
+
163
+ it("should not call the API twice when two concurrent calls have the same missing group", async () => {
164
+ expect.assertions(3);
165
+
166
+ const storedGroupsDtos = defaultGroupsDtos(3);
167
+ const storedCollection = new GroupsCollection(storedGroupsDtos);
168
+ await findAndUpdateGroupsLocalStorageService.groupLocalStorage.set(storedCollection);
169
+
170
+ const missingGroupDto = defaultGroupDto();
171
+ let resolveApiCall;
172
+ const apiPromise = new Promise((resolve) => (resolveApiCall = resolve));
173
+
174
+ jest
175
+ .spyOn(findAndUpdateGroupsLocalStorageService.groupApiService, "findAll")
176
+ .mockImplementationOnce(() => apiPromise);
177
+
178
+ const requestedIds = [storedGroupsDtos[0].id, missingGroupDto.id];
179
+
180
+ const promise1 = findAndUpdateGroupsLocalStorageService.findForLocalStorageByIds(requestedIds);
181
+ const promise2 = findAndUpdateGroupsLocalStorageService.findForLocalStorageByIds(requestedIds);
182
+
183
+ resolveApiCall({ body: [missingGroupDto] });
184
+
185
+ const [result1, result2] = await Promise.all([promise1, promise2]);
186
+
187
+ expect(findAndUpdateGroupsLocalStorageService.groupApiService.findAll).toHaveBeenCalledTimes(1);
188
+ expect(result1.items.map((g) => g.id)).toStrictEqual(requestedIds);
189
+ expect(result2.items.map((g) => g.id)).toStrictEqual(requestedIds);
190
+ });
191
+
192
+ it("should throw if the parameter is not a valid array of UUIDs", async () => {
193
+ expect.assertions(1);
194
+
195
+ await expect(findAndUpdateGroupsLocalStorageService.findForLocalStorageByIds(["not-a-uuid"])).rejects.toThrow();
196
+ });
197
+ });
116
198
  });
@@ -12,7 +12,7 @@
12
12
  * @since 5.7.0
13
13
  */
14
14
 
15
- import GroupsCollection from "../../model/entity/group/groupsCollection";
15
+ import GroupsCollection from "passbolt-styleguide/src/shared/models/entity/group/groupsCollection";
16
16
  import { assertBoolean, assertType } from "../../utils/assertions";
17
17
  import GroupApiService from "../api/group/groupApiService";
18
18
  import User from "../../model/user";
@@ -33,23 +33,19 @@ export default class FindGroupsService {
33
33
  *
34
34
  * @param {Object|null} [contains] optional
35
35
  * @param {Object|null} [filters] optional
36
- * @param {Object|null} [orders] optional
37
36
  * @param {boolean?} [ignoreInvalidEntity] Should invalid entities be ignored.
38
37
  * @returns {Promise<GroupsCollection>}
39
38
  */
40
- async findAll(contains, filters, orders, ignoreInvalidEntity) {
39
+ async findAll(contains, filters, ignoreInvalidEntity) {
41
40
  if (contains) {
42
41
  assertType(contains, Object);
43
42
  }
44
43
  if (filters) {
45
44
  assertType(filters, Object);
46
45
  }
47
- if (orders) {
48
- assertType(orders, Object);
49
- }
50
46
  assertBoolean(ignoreInvalidEntity);
51
- const groupsDto = await this.groupApiService.findAll(contains, filters, orders);
52
- return new GroupsCollection(groupsDto, { clone: false, ignoreInvalidEntity: ignoreInvalidEntity });
47
+ const response = await this.groupApiService.findAll(contains, filters);
48
+ return new GroupsCollection(response.body ?? [], { clone: false, ignoreInvalidEntity: ignoreInvalidEntity });
53
49
  }
54
50
 
55
51
  /**
@@ -72,6 +68,6 @@ export default class FindGroupsService {
72
68
  */
73
69
  async findAllForLocalStorage() {
74
70
  const contains = { groups_users: true, my_group_user: true, modifier: false };
75
- return await this.findAll(contains, null, null, true);
71
+ return await this.findAll(contains, null, true);
76
72
  }
77
73
  }
@@ -12,7 +12,7 @@
12
12
  * @since 5.7.0
13
13
  */
14
14
 
15
- import { defaultGroupsDtos } from "../../model/entity/group/groupsCollection.test.data";
15
+ import { defaultGroupsDtos } from "passbolt-styleguide/src/shared/models/entity/group/groupsCollection.test.data";
16
16
 
17
17
  /**
18
18
  * Helper function to generate mock group data for testing.
@@ -15,7 +15,7 @@
15
15
  import User from "../../model/user";
16
16
  import AccountEntity from "../../model/entity/account/accountEntity";
17
17
  import { defaultAccountDto } from "../../model/entity/account/accountEntity.test.data";
18
- import GroupsCollection from "../../model/entity/group/groupsCollection";
18
+ import GroupsCollection from "passbolt-styleguide/src/shared/models/entity/group/groupsCollection";
19
19
  import BuildApiClientOptionsService from "../account/buildApiClientOptionsService";
20
20
  import FindGroupsService from "./findGroupsService";
21
21
  import MockExtension from "../../../../../test/mocks/mockExtension";
@@ -36,40 +36,35 @@ describe("FindGroupsService", () => {
36
36
  /**
37
37
  * Tests the findAll method of the findGroupsService
38
38
  */
39
- const testFindAll = async (contains, filters, orders, ignoreInvalidEntity) => {
39
+ const testFindAll = async (contains, filters, ignoreInvalidEntity) => {
40
40
  // Setup mock data for the test
41
41
  const groupsDtos = setupMockData();
42
42
  // Create a spy on the findAll method of groupApiService and mock its return value
43
43
  const spy = jest.spyOn(findGroupsService.groupApiService, "findAll").mockResolvedValue(groupsDtos);
44
44
  // Call the findAll method of findGroupsService with the provided parameters
45
- const result = await findGroupsService.findAll(contains, filters, orders, ignoreInvalidEntity);
45
+ const result = await findGroupsService.findAll(contains, filters, ignoreInvalidEntity);
46
46
  // Assert that the spy was called with the correct parameters
47
- expect(spy).toHaveBeenCalledWith(contains, filters, orders);
47
+ expect(spy).toHaveBeenCalledWith(contains, filters);
48
48
  // Assert that the result is an instance of GroupsCollection
49
49
  expect(result).toBeInstanceOf(GroupsCollection);
50
50
  // Assert that the result's id matches the mock data's id
51
51
  expect(result.id).toBe(groupsDtos.id);
52
52
  };
53
53
 
54
- it("should find groups with contains, filters, and orders", async () => {
55
- await testFindAll(
56
- { groups_users: true, my_group_user: true, modifier: false },
57
- { "has-users": "user123" },
58
- { name: "asc" },
59
- true,
60
- );
54
+ it("should find groups with contains and filters", async () => {
55
+ await testFindAll({ groups_users: true, my_group_user: true, modifier: false }, { "has-users": "user123" }, true);
61
56
  });
62
57
 
63
58
  it("should find groups with empty contains, filters, and orders", async () => {
64
- await testFindAll({}, {}, {}, true);
59
+ await testFindAll({}, {}, true);
65
60
  });
66
61
 
67
62
  it("should find groups with null contains, filters, and orders", async () => {
68
- await testFindAll(null, null, null, true);
63
+ await testFindAll(null, null, true);
69
64
  });
70
65
 
71
66
  it("should find groups with ignoreInvalidEntity set to false", async () => {
72
- await testFindAll({ groups_users: true, my_group_user: true, modifier: false }, null, null, false);
67
+ await testFindAll({ groups_users: true, my_group_user: true, modifier: false }, null, false);
73
68
  });
74
69
 
75
70
  /*
@@ -125,7 +120,7 @@ describe("FindGroupsService", () => {
125
120
 
126
121
  const result = await findGroupsService.findAllForLocalStorage();
127
122
 
128
- expect(findGroupsService.findAll).toHaveBeenCalledWith(contains, null, null, true);
123
+ expect(findGroupsService.findAll).toHaveBeenCalledWith(contains, null, true);
129
124
  expect(result).toBeInstanceOf(Array);
130
125
  expect(result.id).toBe(groupsDtos.id);
131
126
  });