Files
hanzo.ai/e2e/gates/guest.spec.ts

91 lines
4.2 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { serveExport } from './export'
import { bare } from './heard'
import { asked, issuer, type Seen } from './issuer'
/**
* A `<slug>.hanzo.app` ALIAS SIGNS IN ON HANZO.AI.
*
* The Sites plane serves this export at cloud-hanzo-ai.hanzo.app and
* hanzo-bot.hanzo.app as well as at its own hosts. No OAuth client lists a
* callback under hanzo.app, so an authorize from there is refused. The alias is
* answered from the export on this machine (.app is HSTS-preloaded, so it is
* routed rather than resolved); hanzo.ai and IAM are answered by stubs, so
* what is measured is where the browser was sent.
*/
const ALIAS = 'cloud-hanzo-ai.hanzo.app'
let port: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
port = new URL(served.url).port
stop = served.close
})
test.afterAll(async () => stop())
/** Every address outside the export is stubbed or refused; returns where the browser went. */
async function hermetic(page: Page): Promise<{ home: string[]; seen: Seen }> {
await page.route('**/*', (route) => route.abort())
await page.route(`https://${ALIAS}/**`, async (route) => {
const u = new URL(route.request().url())
// The page may leave (the ask goes to hanzo.ai) while a file of the one it
// leaves is still being fetched; that request is gone, and so is its answer.
const res = await route.fetch({ url: `http://127.0.0.1:${port}${u.pathname}${u.search}` }).catch(() => null)
return res ? route.fulfill({ response: res }).catch(() => {}) : route.abort().catch(() => {})
})
const home: string[] = []
await page.route('https://hanzo.ai/**', (route) => {
home.push(route.request().url())
return route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><title>Hanzo</title>' })
})
return { home, seen: await issuer(page) }
}
// The Sites plane roots the cloud-hanzo-ai and hanzo-bot aliases at cloud.html and
// bot.html (deploy.yml), so each landing is asked the same question as the home.
for (const landing of ['/', '/cloud', '/bot']) test(`an ask typed on the alias at ${landing} is asked on hanzo.ai, and no authorize leaves the alias`, async ({ page }) => {
const sent = await hermetic(page)
await page.goto(`https://${ALIAS}${landing}`)
// Typed once React owns the composer: text filled into the exported field before
// hydration is the markup's, and hydration puts the empty one back.
await page.locator('html[data-hydrated]').waitFor({ state: 'attached' })
// The ask: the composer, wherever the page draws it (on the home, below the
// hero's sign-in, which must not leave the alias before anything is typed).
const form = page.locator('form[data-slot="prism"]:visible').first()
await form.locator('input:visible, textarea:visible').first().fill('hello there')
await form.locator('button[type=submit], button:not([type])').first().click()
await page.waitForURL(/^https:\/\/hanzo\.ai\/chat\?q=hello/)
expect(sent.home).toHaveLength(1)
expect(asked(sent.seen), 'an authorize from the alias is refused by IAM').toEqual([])
})
test('a room asked for on the alias is the same room on hanzo.ai', async ({ page }) => {
const sent = await hermetic(page)
await page.goto(`https://${ALIAS}/chat?q=hi`)
await page.waitForURL((u) => bare(u.href) === 'https://hanzo.ai/chat?q=hi')
expect(asked(sent.seen)).toEqual([])
})
test('/login on the alias is /login on hanzo.ai', async ({ page }) => {
const sent = await hermetic(page)
await page.goto(`https://${ALIAS}/login`)
await page.waitForURL((u) => bare(u.href) === 'https://hanzo.ai/login')
expect(asked(sent.seen)).toEqual([])
})
test('a product page reads its star count from the build, not from api.github.com', async ({ page }) => {
const github: string[] = []
await hermetic(page)
page.on('request', (r) => {
if (new URL(r.url()).hostname === 'api.github.com') github.push(r.url())
})
// /bot is the short explainer; the product page with its footer is /bot/details.
await page.goto(`https://${ALIAS}/bot/details`)
await page.getByTestId('oss-attribution').first().scrollIntoViewIfNeeded()
await page.waitForTimeout(1500)
expect(github, 'every reader would spend one of 60 unauthenticated requests an hour').toEqual([])
})