Files
hanzo.ai/e2e/unit/support.spec.ts
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

141 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test'
import { assumed, follow, superAdmin, support } from '@hanzo/app/lib/session'
/**
* Support mode is IAM's `assume` and `release`, drawn only for the person IAM
* signs as a SuperAdmin: the token's `owner` is the reserved `admin` org. A
* membership of `admin` is not that. Stepping in keeps the token IAM answers,
* with its own expiry, and selects the organization; a refusal changes nothing.
*/
/** An unsigned token carrying `claims` — session.ts reads claims, it never verifies. */
const token = (claims: Record<string, unknown>) =>
`e30.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.sig`
const OPERATOR = token({ sub: 'u-a', name: 'a', owner: 'admin', orgs: [{ org: 'admin', role: 'admin' }], exp: 2_000_000_000 })
const INSIDE = token({ sub: 'u-a', name: 'a', owner: 'admin', assumed: 'webby-ai', orgs: [{ org: 'admin', role: 'admin' }, { org: 'webby-ai', role: 'admin' }], exp: 2_000_000_100 })
const MEMBER_OF_ADMIN = token({ sub: 'u-z', name: 'z', owner: 'hanzo', orgs: [{ org: 'hanzo', role: 'admin' }, { org: 'admin', role: 'admin' }], exp: 2_000_000_000 })
/** A sign-in token: IAM puts the APPLICATION's org in `owner`, the person's own org first in `orgs`. */
const OPERATOR_SIGNED_IN = token({ sub: 'u-a', name: 'a', owner: 'hanzo', orgs: [{ org: 'admin', role: 'admin' }], exp: 2_000_000_000 })
const MEMBER_VIA_ADMIN_APP = token({ sub: 'u-m', name: 'm', owner: 'admin', orgs: [{ org: 'hanzo', role: 'member' }], exp: 2_000_000_000 })
/** A browser with `access` stored, recording what the page does to it. */
function browser(access: string) {
const store: Record<string, string> = { hanzo_iam_access_token: access, hanzo_iam_expires_at: '1' }
const done = { reloaded: 0 }
const g = globalThis as unknown as { window?: unknown }
const was = g.window
g.window = {
localStorage: {
getItem: (k: string) => store[k] ?? null,
setItem: (k: string, v: string) => {
store[k] = v
},
},
location: { hostname: 'hanzo.ai', reload: () => done.reloaded++ },
dispatchEvent: () => true,
}
return { store, done, restore: () => (g.window = was) }
}
/** IAM answering `status` with `body`, recording what it was asked. */
function iam(status: number, body: unknown) {
const asked: { url: string; init?: RequestInit }[] = []
const was = globalThis.fetch
globalThis.fetch = async (input: string | URL | Request, init?: RequestInit) => {
asked.push({ url: String(input), init })
return new Response(JSON.stringify(body), { status })
}
return { asked, restore: () => (globalThis.fetch = was) }
}
test('a SuperAdmin is the person whose own org is admin — not a membership of admin, and not the app org in `owner`', () => {
for (const [t, want] of [
[OPERATOR, true],
[OPERATOR_SIGNED_IN, true],
[MEMBER_OF_ADMIN, false],
[MEMBER_VIA_ADMIN_APP, false],
] as const) {
const b = browser(t)
try {
expect(superAdmin()).toBe(want)
} finally {
b.restore()
}
}
})
test('stepping in asks IAM to assume, keeps its token with its own expiry, and selects the org', async () => {
const b = browser(OPERATOR)
const answer = iam(200, { status: 'ok', msg: '', data: { accessToken: INSIDE, expiresIn: 3600, assumed: 'webby-ai' } })
try {
await support('webby-ai')
expect(answer.asked).toHaveLength(1)
expect(answer.asked[0].url).toBe('https://api.hanzo.ai/v1/iam/assume')
expect(answer.asked[0].init?.method).toBe('POST')
expect(JSON.parse(String(answer.asked[0].init?.body))).toEqual({ org: 'webby-ai' })
const headers = answer.asked[0].init?.headers as Record<string, string> | undefined
expect(headers?.Authorization).toBe(`Bearer ${OPERATOR}`)
expect(b.store.hanzo_iam_access_token).toBe(INSIDE)
expect(b.store.hanzo_iam_expires_at).toBe(String(2_000_000_100 * 1000))
expect(b.store.hanzo_iam_current_org).toBe('webby-ai')
expect(b.done.reloaded).toBe(1)
expect(assumed()).toBe('webby-ai')
} finally {
answer.restore()
b.restore()
}
})
test('stepping out asks IAM to release and returns to the operator’s own org', async () => {
const b = browser(INSIDE)
const answer = iam(200, { status: 'ok', msg: '', data: { accessToken: OPERATOR, expiresIn: 3600 } })
try {
await support(null)
expect(answer.asked[0].url).toBe('https://api.hanzo.ai/v1/iam/release')
expect(b.store.hanzo_iam_access_token).toBe(OPERATOR)
expect(b.store.hanzo_iam_current_org).toBe('admin')
expect(assumed()).toBeNull()
} finally {
answer.restore()
b.restore()
}
})
test('a refusal from IAM says why and leaves the session as it was', async () => {
const b = browser(MEMBER_OF_ADMIN)
const answer = iam(403, { status: 'error', msg: 'only a platform operator may step into an organization' })
try {
await expect(support('webby-ai')).rejects.toThrow('only a platform operator may step into an organization')
expect(b.store.hanzo_iam_access_token).toBe(MEMBER_OF_ADMIN)
expect(b.store.hanzo_iam_current_org).toBeUndefined()
expect(b.done.reloaded).toBe(0)
} finally {
answer.restore()
b.restore()
}
})
test('another tab stepping in or out is a move for this tab; a plain refresh is not', () => {
const b = browser(INSIDE)
const g = globalThis as unknown as { window: Record<string, unknown> }
const heard: ((e: unknown) => void)[] = []
g.window.addEventListener = (_: string, fn: (e: unknown) => void) => heard.push(fn)
g.window.removeEventListener = () => {}
;(g.window.location as Record<string, unknown>).pathname = '/chat'
const storageArea = g.window.localStorage
let moved = 0
try {
follow('u-a', () => moved++)
const REFRESHED = token({ sub: 'u-a', name: 'a', owner: 'admin', assumed: 'webby-ai', orgs: [{ org: 'admin', role: 'admin' }, { org: 'webby-ai', role: 'admin' }], exp: 2_000_000_200 })
b.store.hanzo_iam_access_token = REFRESHED
heard[0]({ storageArea, key: 'hanzo_iam_access_token' })
expect(moved).toBe(0)
b.store.hanzo_iam_access_token = OPERATOR
heard[0]({ storageArea, key: 'hanzo_iam_access_token' })
expect(moved).toBe(1)
} finally {
b.restore()
}
})