@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.
- package/.changelog.draft +261 -0
- package/.release-it.json +34 -0
- package/CHANGELOG.md +269 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/babel.config.js +17 -0
- package/locales/de/LC_MESSAGES/volto.po +1162 -0
- package/locales/en/LC_MESSAGES/volto.po +1162 -0
- package/locales/es/LC_MESSAGES/volto.po +1169 -0
- package/locales/pt_BR/LC_MESSAGES/volto.po +1167 -0
- package/locales/volto.pot +1164 -0
- package/news/.changelog_template.jinja +15 -0
- package/news/.gitkeep +0 -0
- package/package.json +66 -0
- package/public/.gitkeep +0 -0
- package/src/actions/actions.test.ts +226 -0
- package/src/actions/index.ts +530 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/Applications/Applications.tsx +169 -0
- package/src/components/Applications/ApplicationsPanel.scss +59 -0
- package/src/components/Applications/ApplicationsPanel.stories.tsx +82 -0
- package/src/components/Applications/ApplicationsPanel.test.tsx +235 -0
- package/src/components/Applications/ApplicationsPanel.tsx +342 -0
- package/src/components/Callback/Callback.scss +17 -0
- package/src/components/Callback/Callback.stories.tsx +43 -0
- package/src/components/Callback/Callback.test.tsx +262 -0
- package/src/components/Callback/Callback.tsx +170 -0
- package/src/components/Consent/Consent.test.tsx +75 -0
- package/src/components/Consent/Consent.tsx +98 -0
- package/src/components/Consent/ConsentPanel.scss +37 -0
- package/src/components/Consent/ConsentPanel.stories.tsx +80 -0
- package/src/components/Consent/ConsentPanel.test.tsx +138 -0
- package/src/components/Consent/ConsentPanel.tsx +162 -0
- package/src/components/ControlPanel/ClientsControlPanel.stories.tsx +38 -0
- package/src/components/ControlPanel/ClientsControlPanel.tsx +321 -0
- package/src/components/ControlPanel/ClientsPanel.scss +21 -0
- package/src/components/ControlPanel/ClientsPanel.stories.tsx +71 -0
- package/src/components/ControlPanel/ClientsPanel.test.tsx +267 -0
- package/src/components/ControlPanel/ClientsPanel.tsx +386 -0
- package/src/components/ControlPanel/ConfirmModal.stories.tsx +35 -0
- package/src/components/ControlPanel/ConfirmModal.test.tsx +66 -0
- package/src/components/ControlPanel/ConfirmModal.tsx +68 -0
- package/src/components/ControlPanel/ProviderForm.stories.tsx +107 -0
- package/src/components/ControlPanel/ProvidersControlPanel.scss +26 -0
- package/src/components/ControlPanel/ProvidersControlPanel.stories.tsx +88 -0
- package/src/components/ControlPanel/ProvidersControlPanel.tsx +600 -0
- package/src/components/ControlPanel/RenderUsers.stories.tsx +112 -0
- package/src/components/ControlPanel/RenderUsers.test.tsx +145 -0
- package/src/components/ControlPanel/RenderUsers.tsx +263 -0
- package/src/components/ControlPanel/SecretReveal.scss +32 -0
- package/src/components/ControlPanel/SecretReveal.stories.tsx +33 -0
- package/src/components/ControlPanel/SecretReveal.test.tsx +101 -0
- package/src/components/ControlPanel/SecretReveal.tsx +105 -0
- package/src/components/ControlPanel/UserAccount.stories.tsx +69 -0
- package/src/components/ControlPanel/UserAccount.test.tsx +117 -0
- package/src/components/ControlPanel/UserAccount.tsx +125 -0
- package/src/components/ControlPanel/UserAccountPanel.scss +38 -0
- package/src/components/ControlPanel/UserAccountPanel.stories.tsx +77 -0
- package/src/components/ControlPanel/UserAccountPanel.test.tsx +173 -0
- package/src/components/ControlPanel/UserAccountPanel.tsx +273 -0
- package/src/components/FirstLogin/FirstLogin.scss +12 -0
- package/src/components/FirstLogin/FirstLogin.stories.tsx +50 -0
- package/src/components/FirstLogin/FirstLogin.tsx +94 -0
- package/src/components/Identities/Identities.stories.tsx +51 -0
- package/src/components/Identities/Identities.test.tsx +134 -0
- package/src/components/Identities/Identities.tsx +217 -0
- package/src/components/Identities/IdentitiesList.scss +49 -0
- package/src/components/Identities/IdentitiesList.stories.tsx +71 -0
- package/src/components/Identities/IdentitiesList.test.tsx +218 -0
- package/src/components/Identities/IdentitiesList.tsx +207 -0
- package/src/components/Identities/ProfileEmails.scss +62 -0
- package/src/components/Identities/ProfileEmails.stories.tsx +75 -0
- package/src/components/Identities/ProfileEmails.test.tsx +132 -0
- package/src/components/Identities/ProfileEmails.tsx +217 -0
- package/src/components/Login/Login.stories.tsx +63 -0
- package/src/components/Login/Login.test.tsx +189 -0
- package/src/components/Login/Login.tsx +163 -0
- package/src/components/Login/LoginForm.scss +39 -0
- package/src/components/Login/LoginForm.stories.tsx +112 -0
- package/src/components/Login/LoginForm.test.tsx +483 -0
- package/src/components/Login/LoginForm.tsx +321 -0
- package/src/components/Login/LoginOverlay.scss +44 -0
- package/src/components/Login/LoginOverlay.stories.tsx +73 -0
- package/src/components/Login/LoginOverlay.test.tsx +98 -0
- package/src/components/Login/LoginOverlay.tsx +107 -0
- package/src/components/Login/LoginPanel.scss +50 -0
- package/src/components/Login/LoginPanel.stories.tsx +40 -0
- package/src/components/Login/LoginPanel.tsx +60 -0
- package/src/components/Login/MagicLinkForm.scss +25 -0
- package/src/components/Login/MagicLinkForm.stories.tsx +27 -0
- package/src/components/Login/MagicLinkForm.tsx +133 -0
- package/src/components/Login/PasswordForm.scss +82 -0
- package/src/components/Login/PasswordForm.stories.tsx +26 -0
- package/src/components/Login/PasswordForm.tsx +159 -0
- package/src/components/Login/ProviderButton.scss +103 -0
- package/src/components/Login/ProviderButton.stories.tsx +74 -0
- package/src/components/Login/ProviderButton.test.tsx +99 -0
- package/src/components/Login/ProviderButton.tsx +97 -0
- package/src/components/ProfileGate/ProfileGate.stories.tsx +118 -0
- package/src/components/ProfileGate/ProfileGate.test.tsx +370 -0
- package/src/components/ProfileGate/ProfileGate.tsx +172 -0
- package/src/components/Toolbar/PersonalTools.stories.tsx +111 -0
- package/src/components/Toolbar/PersonalTools.test.tsx +165 -0
- package/src/components/Toolbar/PersonalTools.tsx +177 -0
- package/src/components/Toolbar/Toolbar.jsx +766 -0
- package/src/components/UserMenu/ApplicationsMenuItem.stories.tsx +62 -0
- package/src/components/UserMenu/ApplicationsMenuItem.test.tsx +109 -0
- package/src/components/UserMenu/ApplicationsMenuItem.tsx +82 -0
- package/src/components/UserMenu/IdentitiesMenuItem.stories.tsx +35 -0
- package/src/components/UserMenu/IdentitiesMenuItem.test.tsx +103 -0
- package/src/components/UserMenu/IdentitiesMenuItem.tsx +34 -0
- package/src/components/UserMenu/MenuItem.stories.tsx +32 -0
- package/src/components/UserMenu/MenuItem.tsx +48 -0
- package/src/components/UserMenu/ProfileMenuItem.stories.tsx +76 -0
- package/src/components/UserMenu/ProfileMenuItem.test.tsx +90 -0
- package/src/components/UserMenu/ProfileMenuItem.tsx +60 -0
- package/src/components/UserMenu/UserAvatar.scss +23 -0
- package/src/components/UserMenu/UserAvatar.stories.tsx +121 -0
- package/src/components/UserMenu/UserAvatar.test.tsx +120 -0
- package/src/components/UserMenu/UserAvatar.tsx +119 -0
- package/src/components/UserMenu/UserMenuPlugs.stories.tsx +101 -0
- package/src/components/UserMenu/UserMenuPlugs.test.tsx +202 -0
- package/src/components/UserMenu/UserMenuPlugs.tsx +142 -0
- package/src/components/UserMenu/UserProfileLoader.test.tsx +211 -0
- package/src/components/UserMenu/UserProfileLoader.tsx +54 -0
- package/src/components/Views/GroupView.scss +36 -0
- package/src/components/Views/GroupView.stories.tsx +98 -0
- package/src/components/Views/GroupView.test.tsx +137 -0
- package/src/components/Views/GroupView.tsx +178 -0
- package/src/components/Views/ProfileView.scss +27 -0
- package/src/components/Views/ProfileView.stories.tsx +34 -0
- package/src/components/Views/ProfileView.test.tsx +82 -0
- package/src/components/Views/ProfileView.tsx +100 -0
- package/src/components/Widgets/ProviderIconWidget.scss +21 -0
- package/src/components/Widgets/ProviderIconWidget.stories.tsx +30 -0
- package/src/components/Widgets/ProviderIconWidget.test.tsx +55 -0
- package/src/components/Widgets/ProviderIconWidget.tsx +143 -0
- package/src/config/menu.ts +35 -0
- package/src/config/reducers.ts +7 -0
- package/src/config/routes.test.ts +177 -0
- package/src/config/routes.ts +143 -0
- package/src/config/settings.test.ts +34 -0
- package/src/config/settings.ts +72 -0
- package/src/config/views.ts +30 -0
- package/src/config/widgets.ts +25 -0
- package/src/constants/ActionTypes.ts +42 -0
- package/src/constants/vocabularies.ts +20 -0
- package/src/customizations/volto/components/manage/Controlpanels/Users/RenderUsers.tsx +23 -0
- package/src/customizations/volto/components/manage/Toolbar/PersonalTools.tsx +21 -0
- package/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx +21 -0
- package/src/helpers/avatar.test.ts +79 -0
- package/src/helpers/avatar.ts +85 -0
- package/src/helpers/callback.test.ts +60 -0
- package/src/helpers/callback.ts +71 -0
- package/src/helpers/clientSchema.test.ts +182 -0
- package/src/helpers/clientSchema.ts +181 -0
- package/src/helpers/firstLogin.test.ts +90 -0
- package/src/helpers/firstLogin.ts +76 -0
- package/src/helpers/groupmap.test.ts +81 -0
- package/src/helpers/groupmap.ts +50 -0
- package/src/helpers/identities.ts +22 -0
- package/src/helpers/navigate.test.ts +132 -0
- package/src/helpers/navigate.ts +126 -0
- package/src/helpers/profileGate.test.ts +150 -0
- package/src/helpers/profileGate.ts +191 -0
- package/src/helpers/profileSource.ts +41 -0
- package/src/helpers/propertymap.test.ts +71 -0
- package/src/helpers/propertymap.ts +43 -0
- package/src/helpers/providerSchema.test.ts +410 -0
- package/src/helpers/providerSchema.ts +362 -0
- package/src/helpers/returnUrl.test.ts +42 -0
- package/src/helpers/returnUrl.ts +41 -0
- package/src/helpers/rowmap.ts +110 -0
- package/src/helpers/showPloneLogin.test.ts +78 -0
- package/src/helpers/showPloneLogin.ts +55 -0
- package/src/helpers/token.test.ts +62 -0
- package/src/helpers/token.ts +71 -0
- package/src/index.ts +22 -0
- package/src/reducers/index.ts +369 -0
- package/src/reducers/reducers.test.ts +369 -0
- package/src/stories/fixtures.tsx +640 -0
- package/src/storybook/withPage.test.tsx +90 -0
- package/src/storybook/withPage.tsx +110 -0
- package/src/storybook/withUserMenu.test.tsx +78 -0
- package/src/storybook/withUserMenu.tsx +136 -0
- package/src/styles.css +262 -0
- package/src/testing.tsx +86 -0
- package/src/types.ts +441 -0
- package/towncrier.toml +38 -0
- package/tsconfig.json +22 -0
- package/vitest.config.mjs +47 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authorized-applications list, and one application's details.
|
|
3
|
+
*
|
|
4
|
+
* Structural only: the card, its padding and its title come from the
|
|
5
|
+
* container, and the table is Volto's own. What is left is the detail view,
|
|
6
|
+
* which is the part this page draws for itself.
|
|
7
|
+
*/
|
|
8
|
+
.identity-applications {
|
|
9
|
+
.identity-note {
|
|
10
|
+
margin: 0;
|
|
11
|
+
font-size: 0.875rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
h2 {
|
|
15
|
+
margin: 0 0 0.25rem;
|
|
16
|
+
font-size: 18px;
|
|
17
|
+
font-weight: 500;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
h3 {
|
|
21
|
+
margin: 1.5rem 0 0.5rem;
|
|
22
|
+
font-size: 14px;
|
|
23
|
+
font-weight: 500;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The way back, above the details it leaves. */
|
|
28
|
+
.identity-applications__back {
|
|
29
|
+
display: flex;
|
|
30
|
+
padding: 0;
|
|
31
|
+
border: 0;
|
|
32
|
+
margin-bottom: var(--identity-gap);
|
|
33
|
+
background: transparent;
|
|
34
|
+
color: var(--identity-accent);
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
font: inherit;
|
|
37
|
+
font-size: 0.875rem;
|
|
38
|
+
gap: 0.25rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.identity-applications__claims {
|
|
42
|
+
padding-left: 1.25rem;
|
|
43
|
+
margin: 0 0 0.5rem;
|
|
44
|
+
|
|
45
|
+
li {
|
|
46
|
+
margin-bottom: 0.25rem;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* What withdrawing cannot reach. Once per view rather than per row: it is
|
|
52
|
+
* one fact about this site, not a property of any one application.
|
|
53
|
+
*/
|
|
54
|
+
.identity-applications__lingering {
|
|
55
|
+
padding-top: var(--identity-gap);
|
|
56
|
+
border-top: var(--identity-border);
|
|
57
|
+
margin-top: 2rem;
|
|
58
|
+
font-size: 0.875rem;
|
|
59
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import ApplicationsPanel from './ApplicationsPanel';
|
|
4
|
+
import { FAILED, GRANTS } from '../../stories/fixtures';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The applications a user has authorized.
|
|
8
|
+
*
|
|
9
|
+
* The mirror image of the identities list: that one is what somebody signs
|
|
10
|
+
* in *with*, this is what they signed in *to*.
|
|
11
|
+
*/
|
|
12
|
+
const meta: Meta<typeof ApplicationsPanel> = {
|
|
13
|
+
title: 'Identity/Applications/ApplicationsPanel',
|
|
14
|
+
component: ApplicationsPanel,
|
|
15
|
+
args: {
|
|
16
|
+
grants: GRANTS,
|
|
17
|
+
loading: false,
|
|
18
|
+
selected: null,
|
|
19
|
+
withdrawing: null,
|
|
20
|
+
onSelect: () => {},
|
|
21
|
+
onWithdraw: () => {},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export default meta;
|
|
25
|
+
|
|
26
|
+
type Story = StoryObj<typeof ApplicationsPanel>;
|
|
27
|
+
|
|
28
|
+
/** The listing: a row per application, scanned rather than read. */
|
|
29
|
+
export const Authorized: Story = {};
|
|
30
|
+
|
|
31
|
+
/** One application on its own, which is where the decision is made. */
|
|
32
|
+
export const OneInFull: Story = {
|
|
33
|
+
args: { selected: GRANTS.items[0].client_id },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The detail view of something that reads nothing but your identity. */
|
|
37
|
+
export const OneThatReadsNothing: Story = {
|
|
38
|
+
args: { selected: GRANTS.items[1].client_id },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** An answer somebody opened this page to get. */
|
|
42
|
+
export const NothingAuthorized: Story = {
|
|
43
|
+
args: { grants: { ...GRANTS, items: [] } },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* An agreement outlives the registration it was made with: the operator
|
|
48
|
+
* removed the client, the record stayed, and the user can still clear it.
|
|
49
|
+
*/
|
|
50
|
+
export const AnApplicationThatIsGone: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
grants: {
|
|
53
|
+
...GRANTS,
|
|
54
|
+
items: [{ ...GRANTS.items[0], registered: false, enabled: false }],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Disabled by an operator, so it is being refused already. */
|
|
60
|
+
export const Disabled: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
grants: { ...GRANTS, items: [{ ...GRANTS.items[0], enabled: false }] },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const Withdrawing: Story = {
|
|
67
|
+
args: { withdrawing: GRANTS.items[0].client_id },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** The same, with the details open. */
|
|
71
|
+
export const WithdrawingFromTheDetails: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
selected: GRANTS.items[0].client_id,
|
|
74
|
+
withdrawing: GRANTS.items[0].client_id,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Loading: Story = { args: { grants: null, loading: true } };
|
|
79
|
+
|
|
80
|
+
export const Unavailable: Story = {
|
|
81
|
+
args: { grants: null, error: FAILED.error },
|
|
82
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { fireEvent, render, screen } from '../../testing';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import ApplicationsPanel from './ApplicationsPanel';
|
|
6
|
+
import type { OAuthGrants } from '../../types';
|
|
7
|
+
|
|
8
|
+
const GRANTS: OAuthGrants = {
|
|
9
|
+
'@id': 'http://id.example.org/@oauth-grants',
|
|
10
|
+
access_token_ttl: 900,
|
|
11
|
+
items: [
|
|
12
|
+
{
|
|
13
|
+
'@id': 'http://id.example.org/@oauth-grants/app',
|
|
14
|
+
client_id: 'app',
|
|
15
|
+
title: 'Example App',
|
|
16
|
+
registered: true,
|
|
17
|
+
enabled: true,
|
|
18
|
+
granted_at: '2026-08-01T10:00:00+00:00',
|
|
19
|
+
scopes: [
|
|
20
|
+
{ id: 'openid', claims: [] },
|
|
21
|
+
{ id: 'profile', claims: ['name', 'preferred_username'] },
|
|
22
|
+
{ id: 'email', claims: ['email', 'email_verified'] },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function renderPanel(
|
|
29
|
+
props: Partial<React.ComponentProps<typeof ApplicationsPanel>> = {},
|
|
30
|
+
) {
|
|
31
|
+
const onWithdraw = vi.fn();
|
|
32
|
+
const onSelect = vi.fn();
|
|
33
|
+
render(
|
|
34
|
+
<ApplicationsPanel
|
|
35
|
+
grants={GRANTS}
|
|
36
|
+
loading={false}
|
|
37
|
+
selected={null}
|
|
38
|
+
withdrawing={null}
|
|
39
|
+
onSelect={onSelect}
|
|
40
|
+
onWithdraw={onWithdraw}
|
|
41
|
+
{...props}
|
|
42
|
+
/>,
|
|
43
|
+
);
|
|
44
|
+
return { onWithdraw, onSelect };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The listing with one grant replaced. */
|
|
48
|
+
function withGrant(changes: Partial<OAuthGrants['items'][0]>): OAuthGrants {
|
|
49
|
+
return { ...GRANTS, items: [{ ...GRANTS.items[0], ...changes }] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('ApplicationsPanel', () => {
|
|
53
|
+
it('names each application the way the person saw it', () => {
|
|
54
|
+
renderPanel();
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText('Example App')).toBeTruthy();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('says when they authorized it', () => {
|
|
60
|
+
renderPanel();
|
|
61
|
+
|
|
62
|
+
expect(screen.getByText(/August/)).toBeTruthy();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('counts what each one reads rather than listing it', () => {
|
|
66
|
+
// Four rows of claim lists is a wall nobody reads; the names are one
|
|
67
|
+
// click away, on the view for deciding rather than the one for
|
|
68
|
+
// scanning.
|
|
69
|
+
renderPanel();
|
|
70
|
+
|
|
71
|
+
expect(screen.getByText('4 fields')).toBeTruthy();
|
|
72
|
+
expect(screen.queryByText('preferred_username')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('says an application reads nothing rather than showing a zero', () => {
|
|
76
|
+
renderPanel({
|
|
77
|
+
grants: withGrant({ scopes: [{ id: 'openid', claims: [] }] }),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(screen.getByText('Nothing')).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('opens one application on its own', () => {
|
|
84
|
+
const { onSelect } = renderPanel();
|
|
85
|
+
|
|
86
|
+
fireEvent.click(screen.getByRole('button', { name: 'Details' }));
|
|
87
|
+
|
|
88
|
+
expect(onSelect).toHaveBeenCalledWith('app');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('says what withdrawing cannot reach', () => {
|
|
92
|
+
// An access token already minted is self-encoded with no denylist. A
|
|
93
|
+
// screen promising an instant cutoff would be promising something the
|
|
94
|
+
// server cannot do.
|
|
95
|
+
renderPanel();
|
|
96
|
+
|
|
97
|
+
expect(screen.getByText(/up to 15 minutes/)).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rounds that window up rather than down', () => {
|
|
101
|
+
// Saying "1 minute" for 90 seconds would understate it.
|
|
102
|
+
renderPanel({ grants: { ...GRANTS, access_token_ttl: 90 } });
|
|
103
|
+
|
|
104
|
+
expect(screen.getByText(/up to 2 minutes/)).toBeTruthy();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('offers to withdraw from the listing', () => {
|
|
108
|
+
// Without opening the details first: somebody who recognises the name
|
|
109
|
+
// has already decided.
|
|
110
|
+
const { onWithdraw } = renderPanel();
|
|
111
|
+
|
|
112
|
+
fireEvent.click(screen.getByRole('button', { name: 'Withdraw access' }));
|
|
113
|
+
|
|
114
|
+
expect(onWithdraw).toHaveBeenCalledWith('app');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('takes no second answer while one is on its way', () => {
|
|
118
|
+
renderPanel({ withdrawing: 'app' });
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
(
|
|
122
|
+
screen.getByRole('button', {
|
|
123
|
+
name: 'Withdraw access',
|
|
124
|
+
}) as HTMLButtonElement
|
|
125
|
+
).disabled,
|
|
126
|
+
).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('still offers to withdraw an application that is gone', () => {
|
|
130
|
+
// The agreement outlives the registration, so the record has to be
|
|
131
|
+
// removable or it is one the user can see and not act on.
|
|
132
|
+
renderPanel({ grants: withGrant({ registered: false, enabled: false }) });
|
|
133
|
+
|
|
134
|
+
expect(screen.getByText(/no longer registered/)).toBeTruthy();
|
|
135
|
+
expect(
|
|
136
|
+
screen.getByRole('button', { name: 'Withdraw access' }),
|
|
137
|
+
).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('says what withdrawing cannot reach on the listing too', () => {
|
|
141
|
+
// The caveat belongs wherever the button is.
|
|
142
|
+
renderPanel();
|
|
143
|
+
|
|
144
|
+
expect(screen.getByText(/up to 15 minutes/)).toBeTruthy();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('says when an application is already being refused', () => {
|
|
148
|
+
renderPanel({ grants: withGrant({ enabled: false }) });
|
|
149
|
+
|
|
150
|
+
expect(screen.getByText(/disabled and is being refused/)).toBeTruthy();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('says nothing is authorized rather than showing an empty page', () => {
|
|
154
|
+
// Which is an answer somebody opened this page to get.
|
|
155
|
+
renderPanel({ grants: { ...GRANTS, items: [] } });
|
|
156
|
+
|
|
157
|
+
expect(
|
|
158
|
+
screen.getByText(/have not authorized any application/),
|
|
159
|
+
).toBeTruthy();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('shows one application in full when it is opened', () => {
|
|
163
|
+
renderPanel({ selected: 'app' });
|
|
164
|
+
|
|
165
|
+
expect(screen.getByText('preferred_username')).toBeTruthy();
|
|
166
|
+
expect(screen.getByText(/You authorized this application on/)).toBeTruthy();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('names each claim once however many scopes carry it', () => {
|
|
170
|
+
renderPanel({
|
|
171
|
+
selected: 'app',
|
|
172
|
+
grants: withGrant({
|
|
173
|
+
scopes: [
|
|
174
|
+
{ id: 'profile', claims: ['name'] },
|
|
175
|
+
{ id: 'other', claims: ['name'] },
|
|
176
|
+
],
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(screen.getAllByText('name')).toHaveLength(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('says plainly when an application reads nothing', () => {
|
|
184
|
+
// An empty list under "It can read:" reads as a rendering failure.
|
|
185
|
+
renderPanel({
|
|
186
|
+
selected: 'app',
|
|
187
|
+
grants: withGrant({ scopes: [{ id: 'openid', claims: [] }] }),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(screen.getByText(/Nothing beyond the fact/)).toBeTruthy();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('shows the scopes as well, where the decision is made', () => {
|
|
194
|
+
renderPanel({ selected: 'app' });
|
|
195
|
+
|
|
196
|
+
expect(screen.getByText('openid profile email')).toBeTruthy();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('offers the way back', () => {
|
|
200
|
+
const { onSelect } = renderPanel({ selected: 'app' });
|
|
201
|
+
|
|
202
|
+
fireEvent.click(screen.getByRole('button', { name: /Back to the list/ }));
|
|
203
|
+
|
|
204
|
+
expect(onSelect).toHaveBeenCalledWith(null);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('falls back to the listing when the selection is gone', () => {
|
|
208
|
+
// Which is what a withdrawal leaves behind for one render.
|
|
209
|
+
renderPanel({ selected: 'vanished' });
|
|
210
|
+
|
|
211
|
+
expect(screen.getByText('Example App')).toBeTruthy();
|
|
212
|
+
expect(screen.queryByText('preferred_username')).toBeNull();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('says so while the listing is loading', () => {
|
|
216
|
+
renderPanel({ grants: null, loading: true });
|
|
217
|
+
|
|
218
|
+
expect(screen.getByRole('status').textContent).toContain('Loading');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('waits rather than reporting a failure before the answer arrives', () => {
|
|
222
|
+
renderPanel({ grants: null, loading: false });
|
|
223
|
+
|
|
224
|
+
expect(screen.getByRole('status')).toBeTruthy();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('reports a listing it could not read', () => {
|
|
228
|
+
renderPanel({ grants: null, error: new Error('nope') });
|
|
229
|
+
|
|
230
|
+
expect(screen.getByRole('alert')).toBeTruthy();
|
|
231
|
+
expect(
|
|
232
|
+
screen.queryByRole('button', { name: 'Withdraw access' }),
|
|
233
|
+
).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
});
|