mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
141 lines
6.1 KiB
TypeScript
141 lines
6.1 KiB
TypeScript
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()
|
||
}
|
||
})
|