Files
hanzo.ai/e2e/unit/returned.spec.ts
wbin90620 ea72467377 auth: an OAuth popup returning to / hands its answer to the opener and closes
The app's connect popup (@hanzo/app Setup) returns to
hanzo.ai/?at=-/settings/integrations with connected= or error= on the query.
MOVED moved it on to Settings with the answer dropped, and the popup stayed
open. RETURNED runs first: in a popup it posts the answer to the opener (this
origin only), a BroadcastChannel and storage, then closes the window.
2026-10-26 12:05:47 -04:00

71 lines
2.9 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { RETURNED } from '../../lib/auth/returned'
/**
* AN OAUTH POPUP CLOSES ON ITS RETURN, AND ITS OPENER HEARS HOW IT WENT.
*
* The API's callback sends the popup to `https://hanzo.ai/?at=-/settings/integrations`
* with the answer on the query (cloud apps/provider back.go `returnURL`); `/`'s first
* script hands it on and closes the window (lib/auth/returned). This runs that
* script against a stand-in window, each way the answer travels.
*/
const ORIGIN = 'https://hanzo.ai'
const KEY = 'hanzo-oauth-return'
function run(search: string, { opener = true, name = 'hanzo-oauth-github' } = {}) {
const posted: [unknown, string][] = []
const broadcast: unknown[] = []
const stored: [string, string | null][] = []
let closed = false
const window = {
name,
opener: opener ? { postMessage: (m: unknown, to: string) => posted.push([m, to]) } : null,
close: () => void (closed = true),
}
const location = { search, origin: ORIGIN }
const localStorage = {
setItem: (k: string, v: string) => stored.push([k, v]),
removeItem: (k: string) => stored.push([k, null]),
}
class BroadcastChannel {
constructor(readonly name: string) {}
postMessage(m: unknown) {
if (this.name === KEY) broadcast.push(m)
}
close() {}
}
new Function('window', 'location', 'localStorage', 'BroadcastChannel', RETURNED)(window, location, localStorage, BroadcastChannel)
return { posted, broadcast, stored, closed }
}
test.describe('an OAuth popup returning to /', () => {
test('a connection is told to the opener, on this origin only, and the popup closes', () => {
const r = run('?at=-/settings/integrations&connected=github&account=acme')
const answer = { type: KEY, provider: 'github', ok: true, account: 'acme' }
expect(r.posted).toEqual([[answer, ORIGIN]])
expect(r.broadcast).toEqual([answer])
expect(r.stored[0]).toEqual([KEY, JSON.stringify(answer)])
expect(r.closed).toBe(true)
})
test('a refusal is told the same way, with its reason', () => {
const r = run('?at=-/settings/integrations&error=github&reason=invalid+state')
expect(r.posted).toEqual([[{ type: KEY, provider: 'github', ok: false, reason: 'invalid state' }, ORIGIN]])
expect(r.closed).toBe(true)
})
test('a popup that lost its opener still answers by channel and storage, and closes', () => {
const r = run('?at=-/settings/integrations&connected=github&account=acme', { opener: false })
expect(r.posted).toEqual([])
expect(r.broadcast).toHaveLength(1)
expect(r.closed).toBe(true)
})
test('a tab of its own, or a page with no answer, is left to move on to Settings', () => {
expect(run('?at=-/settings/integrations&connected=github', { opener: false, name: '' }).closed).toBe(false)
expect(run('?at=-/settings/integrations').closed).toBe(false)
expect(run('?error=access_denied').closed).toBe(false)
})
})