feat(SPA): screen to manage API keys (PART 2/3)

This commit is contained in:
fsylvain committed 2026-10-05 00:05:12 +02:00
1 parent 844842fdae
commit 514a082c96
30 files changed
+3844 -83

No files matched your search

+234
View File
@@ -1976,6 +1976,239 @@ const pickerStates: Record<string, VisualState> = {
),
}
const apiKeysFullKey = 'rcms_Kf3x9QwL0aBcT7nV2uYdE5sHjR8mPz4WqXg6C1oIyNe'
const apiKeysTooLongName =
'Nightly import of the new media library from the Brussels office file server'
const apiKeysOwnerList = [
{
id: 4,
user_id: 1,
name: 'Website build',
prefix: 'rcms_Kf3x9Qw',
status: 'active',
created_at: '2026-03-12T09:00:00Z',
last_used_at: '2026-10-03T09:00:00Z',
expires_at: null,
revoked_at: null,
created_by_name: 'Marta Alves',
},
{
id: 2,
user_id: 2,
name: 'Nightly media import',
prefix: 'rcms_Mq7bT2d',
status: 'active',
created_at: '2026-04-02T09:00:00Z',
last_used_at: '2026-10-03T09:00:00Z',
expires_at: '2026-12-31T12:00:00Z',
revoked_at: null,
created_by_name: 'Jonas',
},
{
id: 3,
user_id: 1,
name: 'Staging preview',
prefix: 'rcms_Zt2dL8a',
status: 'expired',
created_at: '2026-06-14T09:00:00Z',
last_used_at: '2026-08-20T09:00:00Z',
expires_at: '2026-08-30T12:00:00Z',
revoked_at: null,
created_by_name: 'Marta Alves',
},
{
id: 1,
user_id: 3,
name: 'Old migration script',
prefix: 'rcms_Hn5wR4c',
status: 'revoked',
created_at: '2026-04-03T09:00:00Z',
last_used_at: '2026-05-09T09:00:00Z',
expires_at: null,
revoked_at: '2026-05-10T09:00:00Z',
created_by_name: 'Inès Duarte',
},
]
const apiKeysAdminList = [
{
id: 2,
user_id: 2,
name: 'Nightly media import',
prefix: 'rcms_Mq7bT2d',
status: 'active',
created_at: '2026-04-02T09:00:00Z',
last_used_at: '2026-10-03T09:00:00Z',
expires_at: '2026-12-31T12:00:00Z',
revoked_at: null,
},
{
id: 1,
user_id: 2,
name: 'Old CI token',
prefix: 'rcms_Lp9cE1x',
status: 'revoked',
created_at: '2026-01-18T09:00:00Z',
last_used_at: '2026-02-02T09:00:00Z',
expires_at: null,
revoked_at: '2026-02-03T09:00:00Z',
},
]
function apiKeysApi(
list: ApiMock,
overrides: Record<string, ApiMock> = {},
session: ApiMock = verifiedSession,
): Record<string, ApiMock> {
return {
'GET /v1/auth/me': session,
'GET /v1/api-key': list,
...overrides,
}
}
const apiKeysOwnerApi = (overrides: Record<string, ApiMock> = {}) =>
apiKeysApi({ status: 200, body: apiKeysOwnerList }, overrides)
const apiKeysCreated: ApiMock = {
status: 201,
body: {
id: 5,
name: 'Nightly media import',
prefix: apiKeysFullKey.slice(0, 12),
key: apiKeysFullKey,
created_at: '2026-10-04T09:00:00Z',
expires_at: null,
},
}
async function openApiKeyForm(page: Page) {
await page.getByRole('button', { name: 'Create key', exact: true }).click()
await page.getByRole('dialog').waitFor()
}
async function fillApiKeyForm(page: Page, name: string, expiry: string) {
await page.locator('input[name="name"]').fill(name)
if (expiry) {
await page.locator('input[name="expires_at"]').fill(expiry)
}
}
async function submitApiKeyForm(page: Page) {
await page.getByRole('dialog').getByRole('button', { name: 'Create key', exact: true }).click()
}
async function settleApiKeyDialog(page: Page) {
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
await page.mouse.move(0, 0)
}
async function revealApiKey(page: Page) {
await openApiKeyForm(page)
await fillApiKeyForm(page, 'Nightly media import', '')
await submitApiKeyForm(page)
await page.getByText('Your new API key').waitFor()
await page.mouse.move(0, 0)
}
function nightlyRevokeButton(page: Page) {
return page
.locator('tbody tr, li')
.filter({ hasText: 'Nightly media import' })
.getByRole('button', { name: 'Revoke', exact: true })
}
async function openApiKeyRevoke(page: Page) {
await nightlyRevokeButton(page).click()
await page.getByRole('alertdialog').waitFor()
await page.mouse.move(0, 0)
}
async function confirmApiKeyRevoke(page: Page) {
await openApiKeyRevoke(page)
await page
.getByRole('alertdialog')
.getByRole('button', { name: 'Revoke key', exact: true })
.click()
await page.getByText('Key revoked').waitFor()
await page.mouse.move(0, 0)
}
const apiKeysStates: Record<string, VisualState> = {
'Owner: list': { path: '/integration/api-keys', api: apiKeysOwnerApi() },
'Admin: own keys only': {
path: '/integration/api-keys',
api: apiKeysApi({ status: 200, body: apiKeysAdminList }, {}, adminSession),
},
Loading: {
path: '/integration/api-keys',
api: apiKeysApi({ status: 200, hang: true }),
},
Empty: { path: '/integration/api-keys', api: apiKeysApi({ status: 200, body: [] }) },
Error: { path: '/integration/api-keys', api: apiKeysApi({ status: 500 }) },
'Create key': {
path: '/integration/api-keys',
api: apiKeysOwnerApi(),
steps: async (page) => {
await openApiKeyForm(page)
await fillApiKeyForm(page, 'Nightly media import', '2026-12-31')
await settleApiKeyDialog(page)
},
},
'Create key: validation': {
path: '/integration/api-keys',
api: apiKeysOwnerApi(),
steps: async (page) => {
await openApiKeyForm(page)
await fillApiKeyForm(page, '', '2026-03-12')
await submitApiKeyForm(page)
await settleApiKeyDialog(page)
},
},
'Create key: name too long': {
path: '/integration/api-keys',
api: apiKeysOwnerApi(),
steps: async (page) => {
await openApiKeyForm(page)
await fillApiKeyForm(page, apiKeysTooLongName, '')
await settleApiKeyDialog(page)
},
},
'One-time reveal': {
path: '/integration/api-keys',
api: apiKeysOwnerApi({ 'POST /v1/api-key': apiKeysCreated }),
steps: revealApiKey,
},
'One-time reveal: copied': {
path: '/integration/api-keys',
origin: integrationOrigin,
api: apiKeysOwnerApi({ 'POST /v1/api-key': apiKeysCreated }),
steps: async (page) => {
await revealApiKey(page)
await page
.getByRole('dialog')
.getByRole('button', { name: 'Copy', exact: true })
.click()
await page
.getByRole('dialog')
.getByRole('button', { name: 'Copied', exact: true })
.waitFor()
await page.mouse.move(0, 0)
},
},
'Revoke: confirm': {
path: '/integration/api-keys',
api: apiKeysOwnerApi(),
steps: openApiKeyRevoke,
},
Revoked: {
path: '/integration/api-keys',
api: apiKeysOwnerApi({ 'DELETE /v1/api-key/2': { status: 204 } }),
steps: confirmApiKeyRevoke,
},
}
export const visualStates: Record<string, Record<string, VisualState>> = {
profile: {
'Account menu': {
@@ -2237,6 +2470,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
},
'Update already running (409)': { path: '/settings', skip: 'clock-sensitive' },
},
'integration-api-keys': apiKeysStates,
integration: {
Default: {
path: '/integration',