@plone-collective/volto-identity 1.0.0-alpha.1

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 (191) hide show
  1. package/.changelog.draft +261 -0
  2. package/.release-it.json +34 -0
  3. package/CHANGELOG.md +269 -0
  4. package/LICENSE +21 -0
  5. package/README.md +228 -0
  6. package/babel.config.js +17 -0
  7. package/locales/de/LC_MESSAGES/volto.po +1162 -0
  8. package/locales/en/LC_MESSAGES/volto.po +1162 -0
  9. package/locales/es/LC_MESSAGES/volto.po +1169 -0
  10. package/locales/pt_BR/LC_MESSAGES/volto.po +1167 -0
  11. package/locales/volto.pot +1164 -0
  12. package/news/.changelog_template.jinja +15 -0
  13. package/news/.gitkeep +0 -0
  14. package/package.json +66 -0
  15. package/public/.gitkeep +0 -0
  16. package/src/actions/actions.test.ts +226 -0
  17. package/src/actions/index.ts +530 -0
  18. package/src/components/.gitkeep +0 -0
  19. package/src/components/Applications/Applications.tsx +169 -0
  20. package/src/components/Applications/ApplicationsPanel.scss +59 -0
  21. package/src/components/Applications/ApplicationsPanel.stories.tsx +82 -0
  22. package/src/components/Applications/ApplicationsPanel.test.tsx +235 -0
  23. package/src/components/Applications/ApplicationsPanel.tsx +342 -0
  24. package/src/components/Callback/Callback.scss +17 -0
  25. package/src/components/Callback/Callback.stories.tsx +43 -0
  26. package/src/components/Callback/Callback.test.tsx +262 -0
  27. package/src/components/Callback/Callback.tsx +170 -0
  28. package/src/components/Consent/Consent.test.tsx +75 -0
  29. package/src/components/Consent/Consent.tsx +98 -0
  30. package/src/components/Consent/ConsentPanel.scss +37 -0
  31. package/src/components/Consent/ConsentPanel.stories.tsx +80 -0
  32. package/src/components/Consent/ConsentPanel.test.tsx +138 -0
  33. package/src/components/Consent/ConsentPanel.tsx +162 -0
  34. package/src/components/ControlPanel/ClientsControlPanel.stories.tsx +38 -0
  35. package/src/components/ControlPanel/ClientsControlPanel.tsx +321 -0
  36. package/src/components/ControlPanel/ClientsPanel.scss +21 -0
  37. package/src/components/ControlPanel/ClientsPanel.stories.tsx +71 -0
  38. package/src/components/ControlPanel/ClientsPanel.test.tsx +267 -0
  39. package/src/components/ControlPanel/ClientsPanel.tsx +386 -0
  40. package/src/components/ControlPanel/ConfirmModal.stories.tsx +35 -0
  41. package/src/components/ControlPanel/ConfirmModal.test.tsx +66 -0
  42. package/src/components/ControlPanel/ConfirmModal.tsx +68 -0
  43. package/src/components/ControlPanel/ProviderForm.stories.tsx +107 -0
  44. package/src/components/ControlPanel/ProvidersControlPanel.scss +26 -0
  45. package/src/components/ControlPanel/ProvidersControlPanel.stories.tsx +88 -0
  46. package/src/components/ControlPanel/ProvidersControlPanel.tsx +600 -0
  47. package/src/components/ControlPanel/RenderUsers.stories.tsx +112 -0
  48. package/src/components/ControlPanel/RenderUsers.test.tsx +145 -0
  49. package/src/components/ControlPanel/RenderUsers.tsx +263 -0
  50. package/src/components/ControlPanel/SecretReveal.scss +32 -0
  51. package/src/components/ControlPanel/SecretReveal.stories.tsx +33 -0
  52. package/src/components/ControlPanel/SecretReveal.test.tsx +101 -0
  53. package/src/components/ControlPanel/SecretReveal.tsx +105 -0
  54. package/src/components/ControlPanel/UserAccount.stories.tsx +69 -0
  55. package/src/components/ControlPanel/UserAccount.test.tsx +117 -0
  56. package/src/components/ControlPanel/UserAccount.tsx +125 -0
  57. package/src/components/ControlPanel/UserAccountPanel.scss +38 -0
  58. package/src/components/ControlPanel/UserAccountPanel.stories.tsx +77 -0
  59. package/src/components/ControlPanel/UserAccountPanel.test.tsx +173 -0
  60. package/src/components/ControlPanel/UserAccountPanel.tsx +273 -0
  61. package/src/components/FirstLogin/FirstLogin.scss +12 -0
  62. package/src/components/FirstLogin/FirstLogin.stories.tsx +50 -0
  63. package/src/components/FirstLogin/FirstLogin.tsx +94 -0
  64. package/src/components/Identities/Identities.stories.tsx +51 -0
  65. package/src/components/Identities/Identities.test.tsx +134 -0
  66. package/src/components/Identities/Identities.tsx +217 -0
  67. package/src/components/Identities/IdentitiesList.scss +49 -0
  68. package/src/components/Identities/IdentitiesList.stories.tsx +71 -0
  69. package/src/components/Identities/IdentitiesList.test.tsx +218 -0
  70. package/src/components/Identities/IdentitiesList.tsx +207 -0
  71. package/src/components/Identities/ProfileEmails.scss +62 -0
  72. package/src/components/Identities/ProfileEmails.stories.tsx +75 -0
  73. package/src/components/Identities/ProfileEmails.test.tsx +132 -0
  74. package/src/components/Identities/ProfileEmails.tsx +217 -0
  75. package/src/components/Login/Login.stories.tsx +63 -0
  76. package/src/components/Login/Login.test.tsx +189 -0
  77. package/src/components/Login/Login.tsx +163 -0
  78. package/src/components/Login/LoginForm.scss +39 -0
  79. package/src/components/Login/LoginForm.stories.tsx +112 -0
  80. package/src/components/Login/LoginForm.test.tsx +483 -0
  81. package/src/components/Login/LoginForm.tsx +321 -0
  82. package/src/components/Login/LoginOverlay.scss +44 -0
  83. package/src/components/Login/LoginOverlay.stories.tsx +73 -0
  84. package/src/components/Login/LoginOverlay.test.tsx +98 -0
  85. package/src/components/Login/LoginOverlay.tsx +107 -0
  86. package/src/components/Login/LoginPanel.scss +50 -0
  87. package/src/components/Login/LoginPanel.stories.tsx +40 -0
  88. package/src/components/Login/LoginPanel.tsx +60 -0
  89. package/src/components/Login/MagicLinkForm.scss +25 -0
  90. package/src/components/Login/MagicLinkForm.stories.tsx +27 -0
  91. package/src/components/Login/MagicLinkForm.tsx +133 -0
  92. package/src/components/Login/PasswordForm.scss +82 -0
  93. package/src/components/Login/PasswordForm.stories.tsx +26 -0
  94. package/src/components/Login/PasswordForm.tsx +159 -0
  95. package/src/components/Login/ProviderButton.scss +103 -0
  96. package/src/components/Login/ProviderButton.stories.tsx +74 -0
  97. package/src/components/Login/ProviderButton.test.tsx +99 -0
  98. package/src/components/Login/ProviderButton.tsx +97 -0
  99. package/src/components/ProfileGate/ProfileGate.stories.tsx +118 -0
  100. package/src/components/ProfileGate/ProfileGate.test.tsx +370 -0
  101. package/src/components/ProfileGate/ProfileGate.tsx +172 -0
  102. package/src/components/Toolbar/PersonalTools.stories.tsx +111 -0
  103. package/src/components/Toolbar/PersonalTools.test.tsx +165 -0
  104. package/src/components/Toolbar/PersonalTools.tsx +177 -0
  105. package/src/components/Toolbar/Toolbar.jsx +766 -0
  106. package/src/components/UserMenu/ApplicationsMenuItem.stories.tsx +62 -0
  107. package/src/components/UserMenu/ApplicationsMenuItem.test.tsx +109 -0
  108. package/src/components/UserMenu/ApplicationsMenuItem.tsx +82 -0
  109. package/src/components/UserMenu/IdentitiesMenuItem.stories.tsx +35 -0
  110. package/src/components/UserMenu/IdentitiesMenuItem.test.tsx +103 -0
  111. package/src/components/UserMenu/IdentitiesMenuItem.tsx +34 -0
  112. package/src/components/UserMenu/MenuItem.stories.tsx +32 -0
  113. package/src/components/UserMenu/MenuItem.tsx +48 -0
  114. package/src/components/UserMenu/ProfileMenuItem.stories.tsx +76 -0
  115. package/src/components/UserMenu/ProfileMenuItem.test.tsx +90 -0
  116. package/src/components/UserMenu/ProfileMenuItem.tsx +60 -0
  117. package/src/components/UserMenu/UserAvatar.scss +23 -0
  118. package/src/components/UserMenu/UserAvatar.stories.tsx +121 -0
  119. package/src/components/UserMenu/UserAvatar.test.tsx +120 -0
  120. package/src/components/UserMenu/UserAvatar.tsx +119 -0
  121. package/src/components/UserMenu/UserMenuPlugs.stories.tsx +101 -0
  122. package/src/components/UserMenu/UserMenuPlugs.test.tsx +202 -0
  123. package/src/components/UserMenu/UserMenuPlugs.tsx +142 -0
  124. package/src/components/UserMenu/UserProfileLoader.test.tsx +211 -0
  125. package/src/components/UserMenu/UserProfileLoader.tsx +54 -0
  126. package/src/components/Views/GroupView.scss +36 -0
  127. package/src/components/Views/GroupView.stories.tsx +98 -0
  128. package/src/components/Views/GroupView.test.tsx +137 -0
  129. package/src/components/Views/GroupView.tsx +178 -0
  130. package/src/components/Views/ProfileView.scss +27 -0
  131. package/src/components/Views/ProfileView.stories.tsx +34 -0
  132. package/src/components/Views/ProfileView.test.tsx +82 -0
  133. package/src/components/Views/ProfileView.tsx +100 -0
  134. package/src/components/Widgets/ProviderIconWidget.scss +21 -0
  135. package/src/components/Widgets/ProviderIconWidget.stories.tsx +30 -0
  136. package/src/components/Widgets/ProviderIconWidget.test.tsx +55 -0
  137. package/src/components/Widgets/ProviderIconWidget.tsx +143 -0
  138. package/src/config/menu.ts +35 -0
  139. package/src/config/reducers.ts +7 -0
  140. package/src/config/routes.test.ts +177 -0
  141. package/src/config/routes.ts +143 -0
  142. package/src/config/settings.test.ts +34 -0
  143. package/src/config/settings.ts +72 -0
  144. package/src/config/views.ts +30 -0
  145. package/src/config/widgets.ts +25 -0
  146. package/src/constants/ActionTypes.ts +42 -0
  147. package/src/constants/vocabularies.ts +20 -0
  148. package/src/customizations/volto/components/manage/Controlpanels/Users/RenderUsers.tsx +23 -0
  149. package/src/customizations/volto/components/manage/Toolbar/PersonalTools.tsx +21 -0
  150. package/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx +21 -0
  151. package/src/helpers/avatar.test.ts +79 -0
  152. package/src/helpers/avatar.ts +85 -0
  153. package/src/helpers/callback.test.ts +60 -0
  154. package/src/helpers/callback.ts +71 -0
  155. package/src/helpers/clientSchema.test.ts +182 -0
  156. package/src/helpers/clientSchema.ts +181 -0
  157. package/src/helpers/firstLogin.test.ts +90 -0
  158. package/src/helpers/firstLogin.ts +76 -0
  159. package/src/helpers/groupmap.test.ts +81 -0
  160. package/src/helpers/groupmap.ts +50 -0
  161. package/src/helpers/identities.ts +22 -0
  162. package/src/helpers/navigate.test.ts +132 -0
  163. package/src/helpers/navigate.ts +126 -0
  164. package/src/helpers/profileGate.test.ts +150 -0
  165. package/src/helpers/profileGate.ts +191 -0
  166. package/src/helpers/profileSource.ts +41 -0
  167. package/src/helpers/propertymap.test.ts +71 -0
  168. package/src/helpers/propertymap.ts +43 -0
  169. package/src/helpers/providerSchema.test.ts +410 -0
  170. package/src/helpers/providerSchema.ts +362 -0
  171. package/src/helpers/returnUrl.test.ts +42 -0
  172. package/src/helpers/returnUrl.ts +41 -0
  173. package/src/helpers/rowmap.ts +110 -0
  174. package/src/helpers/showPloneLogin.test.ts +78 -0
  175. package/src/helpers/showPloneLogin.ts +55 -0
  176. package/src/helpers/token.test.ts +62 -0
  177. package/src/helpers/token.ts +71 -0
  178. package/src/index.ts +22 -0
  179. package/src/reducers/index.ts +369 -0
  180. package/src/reducers/reducers.test.ts +369 -0
  181. package/src/stories/fixtures.tsx +640 -0
  182. package/src/storybook/withPage.test.tsx +90 -0
  183. package/src/storybook/withPage.tsx +110 -0
  184. package/src/storybook/withUserMenu.test.tsx +78 -0
  185. package/src/storybook/withUserMenu.tsx +136 -0
  186. package/src/styles.css +262 -0
  187. package/src/testing.tsx +86 -0
  188. package/src/types.ts +441 -0
  189. package/towncrier.toml +38 -0
  190. package/tsconfig.json +22 -0
  191. package/vitest.config.mjs +47 -0
@@ -0,0 +1,369 @@
1
+ import * as allExports from './index';
2
+ import reducers from './index';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import {
6
+ COMPLETE_CALLBACK,
7
+ CREATE_CLIENT,
8
+ DELETE_CLIENT,
9
+ GET_MY_PROFILE,
10
+ LIST_CLIENTS,
11
+ LIST_KEYS,
12
+ LIST_LOGIN_PROVIDERS,
13
+ ROTATE_CLIENT_SECRET,
14
+ SEND_MAGIC_LINK,
15
+ START_PROVIDER_LOGIN,
16
+ } from '../constants/ActionTypes';
17
+ import {
18
+ clientCreate,
19
+ clientDelete,
20
+ clientSecretRotate,
21
+ identityCallback,
22
+ loginProviders,
23
+ userProfile,
24
+ myProfile,
25
+ magicLinkSend,
26
+ oauthClients,
27
+ providerLogin,
28
+ signingKeys,
29
+ } from './index';
30
+
31
+ describe('the reducer map', () => {
32
+ it('registers every reducer this module exports', () => {
33
+ // The map is what `config.addonReducers` receives, and it is written by
34
+ // hand -- so a reducer can be exported, imported, tested and dispatched
35
+ // to while never being mounted in the store. It fails silently: the
36
+ // request succeeds and the result lands nowhere, which reads as a bug in
37
+ // whatever component was selecting the missing slice. `userProfile`
38
+ // shipped that way and presented as three unrelated bugs in the user
39
+ // menu.
40
+ const exported = Object.entries(allExports)
41
+ .filter(
42
+ ([name, value]) => name !== 'default' && typeof value === 'function',
43
+ )
44
+ .map(([name]) => name);
45
+
46
+ expect(Object.keys(reducers).sort()).toEqual(exported.sort());
47
+ });
48
+ });
49
+
50
+ describe('loginProviders', () => {
51
+ it('starts empty and not loading', () => {
52
+ const state = loginProviders(undefined, { type: 'INIT' });
53
+
54
+ expect(state.data).toEqual([]);
55
+ expect(state.loading).toBe(false);
56
+ expect(state.loaded).toBe(false);
57
+ });
58
+
59
+ it('keeps the items out of the listing', () => {
60
+ const state = loginProviders(undefined, {
61
+ type: `${LIST_LOGIN_PROVIDERS}_SUCCESS`,
62
+ result: { '@id': 'x', items: [{ id: 'dex', driver: 'oidc-generic' }] },
63
+ });
64
+
65
+ expect(state.data).toHaveLength(1);
66
+ expect(state.loaded).toBe(true);
67
+ });
68
+
69
+ it('survives a listing with no items', () => {
70
+ const state = loginProviders(undefined, {
71
+ type: `${LIST_LOGIN_PROVIDERS}_SUCCESS`,
72
+ result: {},
73
+ });
74
+
75
+ expect(state.data).toEqual([]);
76
+ });
77
+
78
+ it('records a failure without keeping stale data', () => {
79
+ const loaded = loginProviders(undefined, {
80
+ type: `${LIST_LOGIN_PROVIDERS}_SUCCESS`,
81
+ result: { items: [{ id: 'dex' }] },
82
+ });
83
+
84
+ const failed = loginProviders(loaded, {
85
+ type: `${LIST_LOGIN_PROVIDERS}_FAIL`,
86
+ error: 'boom',
87
+ });
88
+
89
+ expect(failed.error).toBe('boom');
90
+ expect(failed.data).toEqual([]);
91
+ });
92
+
93
+ it('fills from the identities listing when it carried the component', () => {
94
+ // How the identities page loads in one request rather than two.
95
+ const state = loginProviders(undefined, {
96
+ type: 'IDENTITY_LIST_IDENTITIES_SUCCESS',
97
+ result: {
98
+ '@components': { 'login-providers': { items: [{ id: 'dex' }] } },
99
+ },
100
+ });
101
+
102
+ expect(state.data).toEqual([{ id: 'dex' }]);
103
+ expect(state.loaded).toBe(true);
104
+ });
105
+
106
+ it('keeps what it has when that listing carried no component', () => {
107
+ // The refresh after an unlink does not expand: the providers did not
108
+ // change, and clearing them would blank the buttons for a moment.
109
+ const loaded = loginProviders(undefined, {
110
+ type: 'IDENTITY_LIST_LOGIN_PROVIDERS_SUCCESS',
111
+ result: { items: [{ id: 'dex' }] },
112
+ });
113
+
114
+ const after = loginProviders(loaded, {
115
+ type: 'IDENTITY_LIST_IDENTITIES_SUCCESS',
116
+ result: { items: [] },
117
+ });
118
+
119
+ expect(after.data).toEqual([{ id: 'dex' }]);
120
+ });
121
+
122
+ it('is not put into a loading state by the other request', () => {
123
+ // The pending half belongs to somebody else's request; reacting to it
124
+ // would flicker these buttons on every identities refresh.
125
+ const loaded = loginProviders(undefined, {
126
+ type: 'IDENTITY_LIST_LOGIN_PROVIDERS_SUCCESS',
127
+ result: { items: [{ id: 'dex' }] },
128
+ });
129
+
130
+ const after = loginProviders(loaded, {
131
+ type: 'IDENTITY_LIST_IDENTITIES_PENDING',
132
+ });
133
+
134
+ expect(after.data).toEqual([{ id: 'dex' }]);
135
+ expect(after.loading).toBe(false);
136
+ });
137
+
138
+ it('ignores actions belonging to something else', () => {
139
+ const state = loginProviders(undefined, { type: 'SOMETHING_ELSE' });
140
+
141
+ expect(state.data).toEqual([]);
142
+ });
143
+ });
144
+
145
+ describe('userProfile', () => {
146
+ it('starts empty', () => {
147
+ const state = userProfile(undefined, { type: 'INIT' });
148
+
149
+ expect(state.data).toBeNull();
150
+ expect(state.loaded).toBe(false);
151
+ });
152
+
153
+ it('keeps the whole user, not a chosen few fields', () => {
154
+ // The toolbar wants the portrait, the menu wants profile_url and an
155
+ // administrator view wants identities and source; picking here would
156
+ // mean editing this reducer every time one of them grows.
157
+ const result = {
158
+ id: 'alice',
159
+ portrait: '/portrait.png',
160
+ profile_url: '/identity-profiles/alice',
161
+ source: 'identity_profile',
162
+ identities: [],
163
+ };
164
+
165
+ const state = userProfile(undefined, {
166
+ type: 'IDENTITY_GET_USER_PROFILE_SUCCESS',
167
+ result,
168
+ });
169
+
170
+ expect(state.data).toEqual(result);
171
+ });
172
+
173
+ it('reports a failure rather than holding a stale user', () => {
174
+ const loaded = userProfile(undefined, {
175
+ type: 'IDENTITY_GET_USER_PROFILE_SUCCESS',
176
+ result: { id: 'alice' },
177
+ });
178
+
179
+ const failed = userProfile(loaded, {
180
+ type: 'IDENTITY_GET_USER_PROFILE_FAIL',
181
+ error: 'boom',
182
+ });
183
+
184
+ expect(failed.error).toBe('boom');
185
+ expect(failed.data).toBeNull();
186
+ });
187
+ });
188
+
189
+ describe('providerLogin', () => {
190
+ it('clears the previous URL while a new attempt is pending', () => {
191
+ const loaded = providerLogin(undefined, {
192
+ type: `${START_PROVIDER_LOGIN}_SUCCESS`,
193
+ result: { provider: 'dex', authorize_url: 'https://idp/one' },
194
+ });
195
+
196
+ const pending = providerLogin(loaded, {
197
+ type: `${START_PROVIDER_LOGIN}_PENDING`,
198
+ });
199
+
200
+ // Leaving the old URL in place would send the browser to the previous
201
+ // attempt's authorize URL, whose state has already been consumed.
202
+ expect(pending.data).toBeNull();
203
+ expect(pending.loading).toBe(true);
204
+ });
205
+
206
+ it('keeps the authorize URL on success', () => {
207
+ const state = providerLogin(undefined, {
208
+ type: `${START_PROVIDER_LOGIN}_SUCCESS`,
209
+ result: { provider: 'dex', authorize_url: 'https://idp/auth' },
210
+ });
211
+
212
+ expect(state.data?.authorize_url).toBe('https://idp/auth');
213
+ });
214
+ });
215
+
216
+ describe('identityCallback', () => {
217
+ it('keeps the token and came_from', () => {
218
+ const state = identityCallback(undefined, {
219
+ type: `${COMPLETE_CALLBACK}_SUCCESS`,
220
+ result: { token: 'jwt-value', came_from: '/somewhere' },
221
+ });
222
+
223
+ expect(state.data?.token).toBe('jwt-value');
224
+ expect(state.data?.came_from).toBe('/somewhere');
225
+ });
226
+
227
+ it('marks a refusal as an error rather than a token', () => {
228
+ const state = identityCallback(undefined, {
229
+ type: `${COMPLETE_CALLBACK}_FAIL`,
230
+ error: { status: 401 },
231
+ });
232
+
233
+ expect(state.data).toBeNull();
234
+ expect(state.error).toEqual({ status: 401 });
235
+ });
236
+ });
237
+
238
+ describe('magicLinkSend', () => {
239
+ it('records that a link went out', () => {
240
+ const state = magicLinkSend(undefined, {
241
+ type: `${SEND_MAGIC_LINK}_SUCCESS`,
242
+ result: { sent: true },
243
+ });
244
+
245
+ expect(state.data).toBe(true);
246
+ });
247
+
248
+ it('does not treat a rate-limited refusal as sent', () => {
249
+ const state = magicLinkSend(undefined, {
250
+ type: `${SEND_MAGIC_LINK}_FAIL`,
251
+ error: { status: 429 },
252
+ });
253
+
254
+ expect(state.data).toBe(false);
255
+ expect(state.error).toEqual({ status: 429 });
256
+ });
257
+ });
258
+
259
+ describe('myProfile', () => {
260
+ it('starts with nothing known', () => {
261
+ const state = myProfile(undefined, { type: 'INIT' });
262
+
263
+ expect(state.data).toBeNull();
264
+ expect(state.loaded).toBe(false);
265
+ });
266
+
267
+ it('keeps the whole answer', () => {
268
+ const result = {
269
+ '@id': '/@my-profile',
270
+ userid: 'alice',
271
+ profile: '/identity-profiles/alice',
272
+ review_state: 'incomplete',
273
+ };
274
+
275
+ const state = myProfile(undefined, {
276
+ type: `${GET_MY_PROFILE}_SUCCESS`,
277
+ result,
278
+ });
279
+
280
+ expect(state.data).toEqual(result);
281
+ expect(state.loaded).toBe(true);
282
+ });
283
+
284
+ it('records a failure without pretending it loaded', () => {
285
+ const state = myProfile(undefined, {
286
+ type: `${GET_MY_PROFILE}_FAIL`,
287
+ error: 'nope',
288
+ });
289
+
290
+ expect(state.error).toBe('nope');
291
+ expect(state.loaded).toBe(false);
292
+ });
293
+ });
294
+
295
+ describe('the OAuth client reducers', () => {
296
+ it('keeps the listing items', () => {
297
+ const state = oauthClients(undefined, {
298
+ type: `${LIST_CLIENTS}_SUCCESS`,
299
+ result: { items: [{ client_id: 'app' }], items_total: 1 },
300
+ });
301
+
302
+ expect(state.data).toEqual([{ client_id: 'app' }]);
303
+ });
304
+
305
+ it('keeps the whole client on create, because the secret is on it', () => {
306
+ const result = { client_id: 'app', secret: 's3cr3t' };
307
+
308
+ const state = clientCreate(undefined, {
309
+ type: `${CREATE_CLIENT}_SUCCESS`,
310
+ result,
311
+ });
312
+
313
+ expect(state.data).toEqual(result);
314
+ });
315
+
316
+ it('keeps the whole client on rotation, for the same reason', () => {
317
+ const result = { client_id: 'app', secret: 'fresher' };
318
+
319
+ const state = clientSecretRotate(undefined, {
320
+ type: `${ROTATE_CLIENT_SECRET}_SUCCESS`,
321
+ result,
322
+ });
323
+
324
+ expect(state.data.secret).toBe('fresher');
325
+ });
326
+
327
+ it('clears the previous secret while a new request is pending', () => {
328
+ // Otherwise a failed rotation leaves the last secret on screen looking
329
+ // like it succeeded.
330
+ const loaded = clientSecretRotate(undefined, {
331
+ type: `${ROTATE_CLIENT_SECRET}_SUCCESS`,
332
+ result: { client_id: 'app', secret: 'old' },
333
+ });
334
+
335
+ const pending = clientSecretRotate(loaded, {
336
+ type: `${ROTATE_CLIENT_SECRET}_PENDING`,
337
+ });
338
+
339
+ expect(pending.data).toBeNull();
340
+ });
341
+
342
+ it('records a delete without keeping a body', () => {
343
+ const state = clientDelete(undefined, { type: `${DELETE_CLIENT}_SUCCESS` });
344
+
345
+ expect(state.data).toBe(true);
346
+ expect(state.loaded).toBe(true);
347
+ });
348
+
349
+ it('keeps the ring description', () => {
350
+ const result = { algorithm: 'RS256', items: [], items_total: 0 };
351
+
352
+ const state = signingKeys(undefined, {
353
+ type: `${LIST_KEYS}_SUCCESS`,
354
+ result,
355
+ });
356
+
357
+ expect(state.data).toEqual(result);
358
+ });
359
+
360
+ it('records a failure without pretending it loaded', () => {
361
+ const state = oauthClients(undefined, {
362
+ type: `${LIST_CLIENTS}_FAIL`,
363
+ error: 'nope',
364
+ });
365
+
366
+ expect(state.error).toBe('nope');
367
+ expect(state.loaded).toBe(false);
368
+ });
369
+ });