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