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

70 lines
3.1 KiB
TypeScript

import { expect, test, type Page, type Request } from '@playwright/test'
import { serveExport } from './export'
import { issuer } from './issuer'
/**
* AN AFFILIATE LINK PAYS ONLY IF THIS SITE READS IT.
*
* hanzoai/cloud apps/affiliate mints `https://hanzo.ai/?aff=<code>`, and
* /affiliate promises that an org signing up through it belongs to the
* affiliate. Measured on the shipped bytes: the landing counts the click and
* keeps the code, and after sign-in the code is bound to the org the session
* works in, then dropped.
*/
const CODE = 'acme'
const ORG = 'bobs-org'
function signIn(page: Page) {
return page.evaluate((org) => {
const part = (v: unknown) =>
btoa(JSON.stringify(v)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const exp = Math.floor(Date.now() / 1000) + 3600
const token = `${part({ alg: 'none', typ: 'JWT' })}.${part({ sub: 'bob', name: 'bob', exp, orgs: [{ org }] })}.`
localStorage.setItem('hanzo_iam_access_token', token)
localStorage.setItem('hanzo_iam_id_token', token)
localStorage.setItem('hanzo_iam_expires_at', String(exp * 1000))
}, ORG)
}
test.describe('an affiliate link', () => {
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
test('counts the click, keeps the code, and binds it to the org after sign-in', async ({ page }) => {
const sent: Request[] = []
await page.route('**/v1/**', (route) => {
const url = route.request().url()
if (url.includes('/v1/affiliate/')) sent.push(route.request())
const status = url.includes('/v1/affiliate/attribute') ? 201 : 200
return route.fulfill({ status, contentType: 'application/json', body: '{}' })
})
await issuer(page)
await page.goto(`${base}/?aff=${CODE}`, { waitUntil: 'domcontentloaded' })
// The click is counted from the first commit's effects (app/providers `keep`);
// `data-hydrated` is set after every one of them.
await page.locator('html[data-hydrated]').waitFor({ state: 'attached' })
await expect.poll(() => sent.map((r) => new URL(r.url()).pathname)).toContain('/v1/affiliate/click')
expect(sent[0].postDataJSON()).toEqual({ code: CODE })
expect(await page.evaluate(() => localStorage.getItem('hz_aff'))).toBe(CODE)
expect(sent.some((r) => r.url().includes('/attribute')), 'no session, nothing to bind').toBe(false)
await signIn(page)
await page.goto(`${base}/chat`, { waitUntil: 'domcontentloaded' })
await page.locator('html[data-hydrated]').waitFor({ state: 'attached' })
await expect.poll(() => sent.find((r) => r.url().includes('/v1/affiliate/attribute')) ?? null).not.toBeNull()
const bound = sent.find((r) => r.url().includes('/v1/affiliate/attribute'))!
expect(bound.postDataJSON()).toEqual({ code: CODE })
expect(bound.headers()['x-org-id']).toBe(ORG)
expect(bound.headers()['authorization']).toMatch(/^Bearer /)
await expect.poll(() => page.evaluate(() => localStorage.getItem('hz_aff'))).toBeNull()
})
})