Files
zpan/e2e/oauth-apps.spec.ts
T
Jasper Van 2657f82ef1 feat(auth): add contextual OAuth workspace grants (#550)
* feat(auth): add contextual OAuth workspace grants

* fix(auth): register RFC 9396 detail types

* fix(openapi): restore delegated CLI auth profile

* fix(store): recover paid capacity fulfillment

* fix(auth): close OAuth contract gaps

* fix(store): resume verified x402 settlements

* chore(deps): update zpan cloud sdk to 2.5.2

* test(store): cover verified settlement recovery

* fix(auth): localize standard consent scopes

* fix(docker): include dependency patches before install

* fix(store): harden x402 purchase responses

* test(auth): cover OAuth authorization boundaries

* docs: add PR 550 verification screenshots

* chore: remove temporary verification screenshots

* docs(openapi): document exhausted capacity response
2026-08-02 13:33:15 -04:00

181 lines
7.7 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { signUpAndGoToFiles } from './helpers'
const oauthQuery = new URLSearchParams({
client_id: 'dynamic-client',
redirect_uri: 'https://broker.example.com/callback',
response_type: 'code',
scope: 'openid offline_access objects:read shares:create quota:read',
authorization_details: JSON.stringify([{ type: 'https://zpan.space/authorization-details/workspace' }]),
}).toString()
test.describe('OAuth Apps UI', () => {
test('renders consent details and submits full approval @desktop', async ({ page }) => {
await signUpAndGoToFiles(page)
await page.route('**/api/oauth-consent?*', async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
clientId: 'dynamic-client',
clientName: 'FlareAuth',
clientOrigin: 'https://broker.example.com',
workspaces: [{ id: 'org-e2e', name: 'Personal' }],
requestedWorkspaceIds: [],
scopes: ['objects:read', 'shares:create', 'quota:read'],
standardScopes: ['openid', 'offline_access'],
redirectUri: 'https://broker.example.com/callback',
grantLifetime: { accessTokenSeconds: 900, refreshTokenSeconds: 2_592_000 },
}),
})
})
await page.route('**/api/oauth-consent', async (route) => {
if (route.request().method() !== 'POST') return route.fallback()
expect(route.request().method()).toBe('POST')
const body = route.request().postDataJSON() as { accept: boolean; oauthQuery?: string; workspaceIds?: string[] }
expect(body).toEqual({ accept: true, oauthQuery, workspaceIds: ['org-e2e'] })
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ url: 'https://broker.example.com/callback?code=e2e-code' }),
})
})
await page.route('https://broker.example.com/callback?code=e2e-code', async (route) => {
await route.fulfill({ contentType: 'text/html', body: '<main>Returned to FlareAuth</main>' })
})
await page.goto(`/oauth/consent?${oauthQuery}`)
await expect(page.getByRole('heading', { name: 'FlareAuth wants to access your workspaces' })).toBeVisible()
await expect(page.getByText('https://broker.example.com', { exact: true })).toBeVisible()
await expect(page.getByText('Personal')).toBeVisible()
await expect(page.getByText('Files: read objects')).toBeVisible()
await expect(page.getByText('Shares: create shares')).toBeVisible()
await expect(page.getByText('Quota: read workspace quota')).toBeVisible()
await expect(page.getByRole('navigation')).toHaveCount(0)
const approve = page.getByRole('button', { name: 'Approve Access' })
await expect(approve).toBeDisabled()
await page.getByRole('checkbox', { name: 'Personal' }).check()
await expect(approve).toBeEnabled()
await approve.click()
await expect(page).toHaveURL(/broker\.example\.com\/callback\?code=e2e-code/, { timeout: 10000 })
await expect(page.getByText('Returned to FlareAuth')).toBeVisible()
})
test('lists and revokes delegated grants in settings @desktop', async ({ page }) => {
await signUpAndGoToFiles(page)
let revoked = false
await page.route('**/api/oauth-grants', async (route) => {
if (route.request().method() !== 'GET') return route.fallback()
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
items: revoked
? []
: [
{
id: 'grant-e2e',
clientId: 'dynamic-client',
clientName: 'FlareAuth',
userId: 'user-e2e',
workspaces: [{ id: 'org-e2e', name: 'Personal' }],
scopes: ['objects:read', 'shares:create'],
createdAt: '2026-07-29T12:00:00.000Z',
lastUsedAt: null,
status: 'active',
},
],
}),
})
})
await page.route('**/api/oauth-grants/grant-e2e', async (route) => {
expect(route.request().method()).toBe('DELETE')
revoked = true
await route.fulfill({ status: 204 })
})
await page.goto('/settings/oauth-apps')
await expect(page.getByText('Authorized OAuth Apps')).toBeVisible()
await expect(page.getByRole('cell', { name: 'FlareAuth' })).toBeVisible()
await expect(page.getByText('Shares: create shares')).toBeVisible()
const revokeButtons = page.getByRole('button', { name: 'Revoke' })
await revokeButtons.last().click()
await expect(page.getByRole('dialog', { name: 'Revoke OAuth Grant' })).toBeVisible()
await page.getByRole('dialog').getByRole('button', { name: 'Revoke' }).click()
await expect(page.getByText('No authorized OAuth apps yet')).toBeVisible()
})
test('keeps consent and delegated grants usable on narrow screens @mobile', async ({ page }) => {
await signUpAndGoToFiles(page)
await page.route('**/api/oauth-consent?*', async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
clientId: 'dynamic-client',
clientName: 'FlareAuth',
clientOrigin: 'https://broker.example.com',
workspaces: [{ id: 'org-e2e', name: 'Personal' }],
requestedWorkspaceIds: [],
scopes: ['objects:read', 'shares:create', 'quota:read'],
standardScopes: ['openid', 'offline_access'],
redirectUri: 'https://broker.example.com/callback',
grantLifetime: { accessTokenSeconds: 900, refreshTokenSeconds: 2_592_000 },
}),
})
})
await page.route('**/api/oauth-grants', async (route) => {
if (route.request().method() !== 'GET') return route.fallback()
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
items: [
{
id: 'grant-mobile',
clientId: 'dynamic-client',
clientName: 'FlareAuth',
userId: 'user-e2e',
workspaces: [{ id: 'org-e2e', name: 'Personal' }],
scopes: ['objects:read', 'shares:create', 'quota:read'],
createdAt: '2026-07-29T12:00:00.000Z',
lastUsedAt: '2026-07-29T12:30:00.000Z',
status: 'active',
},
],
}),
})
})
await page.goto(`/oauth/consent?${oauthQuery}`)
await expect(page.getByRole('heading', { name: 'FlareAuth wants to access your workspaces' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Approve Access' })).toBeVisible()
await expect(page.getByText('Files: read objects')).toBeVisible()
await expect(page.getByText('Shares: create shares')).toBeVisible()
await page.getByRole('checkbox', { name: 'Personal' }).check()
await expect
.poll(async () => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1))
.toBe(true)
await page.goto('/settings/oauth-apps')
await expect(page.getByText('Authorized OAuth Apps')).toBeVisible()
await expect(page.getByRole('cell', { name: 'FlareAuth' })).toBeVisible()
const grantsTableContainer = page.locator('[data-slot="table-container"]').last()
await expect(grantsTableContainer).toBeVisible()
await expect
.poll(async () =>
grantsTableContainer.evaluate((node) => (node as HTMLElement).scrollWidth > (node as HTMLElement).clientWidth),
)
.toBe(true)
await expect
.poll(async () => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1))
.toBe(true)
await grantsTableContainer.evaluate((node) => {
node.scrollLeft = node.scrollWidth
})
await expect(page.getByRole('button', { name: 'Revoke' }).last()).toBeVisible()
})
})