mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
A tag that throws at sign-up never keeps the new account on the page
The account exists before its completion is stated, so track() runs inside a try: a throwing pixel is logged, and the browser still leaves for the app that asked. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* The account exists before the completion is stated, so stating it can never
|
||||
* be what keeps the person on this page: a tag that throws inside `track()`
|
||||
* (a broken vendor build, an extension's shim) still lets the browser leave for
|
||||
* the app that asked.
|
||||
*/
|
||||
import { afterEach, test, vi } from 'vitest'
|
||||
import assert from 'node:assert/strict'
|
||||
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import type { Brand } from '@hanzo/id-shared'
|
||||
import { createAuthClient } from '@hanzo/id-auth'
|
||||
import { Signup } from './Signup'
|
||||
|
||||
vi.mock('@hanzo/event', async (actual) => ({
|
||||
...(await actual<typeof import('@hanzo/event')>()),
|
||||
track: () => {
|
||||
throw new Error('pixel threw')
|
||||
},
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
const ORG = {
|
||||
orgId: 'hanzo',
|
||||
iamUrl: 'https://hanzo.id',
|
||||
iamIssuer: 'https://hanzo.id',
|
||||
clientId: 'hanzo-console',
|
||||
appName: 'hanzo-console',
|
||||
publicOrigin: 'https://hanzo.id',
|
||||
brandPackage: '@hanzo/brand',
|
||||
} as Parameters<typeof createAuthClient>[0]['org']
|
||||
|
||||
const BRAND = { name: 'Hanzo', logoUrl: '', faviconUrl: '' } as unknown as Brand
|
||||
const REDIRECT = 'https://hanzo.ai/auth/callback'
|
||||
|
||||
/** IAM registers the account and mints the code the app asked for. */
|
||||
const iam = (async (input: RequestInfo | URL) => {
|
||||
const path = new URL(input.toString()).pathname
|
||||
const body =
|
||||
path === '/v1/iam/signup'
|
||||
? { status: 'ok', data: { id: 'u-1', owner: 'hanzo', name: 'ada' } }
|
||||
: path === '/v1/iam/login'
|
||||
? { status: 'ok', data: 'c-minted' }
|
||||
: { status: 'ok', data: { enableSignUp: true } }
|
||||
return new Response(JSON.stringify(body), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
||||
}) as unknown as typeof fetch
|
||||
|
||||
test('a tag that throws at completion does not keep the new account on this page', async () => {
|
||||
const request = new URLSearchParams({ client_id: 'hanzo-app', redirect_uri: REDIRECT, response_type: 'code', state: 'S' })
|
||||
;(window as unknown as { happyDOM: { setURL(u: string): void } }).happyDOM.setURL(`https://hanzo.id/signup?${request}`)
|
||||
const here = window.location.href
|
||||
const gone: string[] = []
|
||||
Object.defineProperty(window.location, 'href', { configurable: true, get: () => here, set: (u: string) => void gone.push(u) })
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const { container } = render(<Signup client={createAuthClient({ org: ORG, fetchImpl: iam })} brand={BRAND} />)
|
||||
await waitFor(() => assert.ok(container.querySelector('input[type=password]')))
|
||||
await act(async () => {
|
||||
fireEvent.change(container.querySelector('input[type=email]')!, { target: { value: '[email protected]' } })
|
||||
fireEvent.change(container.querySelector('input[type=password]')!, { target: { value: 'correct horse battery staple' } })
|
||||
})
|
||||
await act(async () => void fireEvent.click(container.querySelector('button[type=submit]')!))
|
||||
|
||||
await waitFor(() => assert.equal(gone.length, 1, 'the browser leaves for the app'))
|
||||
assert.ok(gone[0]!.startsWith(REDIRECT), `to the app that asked, got ${gone[0]}`)
|
||||
assert.equal(container.querySelector('[role=alert]')?.textContent ?? '', '', 'and no error is shown')
|
||||
})
|
||||
@@ -162,8 +162,14 @@ export function Signup({ client, brand }: { client: AuthClient; brand: Brand })
|
||||
if (subject) analytics.identify(subject)
|
||||
// ONE moment, under one event id, to our stream and to each of the
|
||||
// brand's tags the visitor's choice allows (GA4 `sign_up`, Meta
|
||||
// `CompleteRegistration`), so no platform counts it twice.
|
||||
track(analytics, EVENTS.SIGNUP_COMPLETED, { method: 'email' })
|
||||
// `CompleteRegistration`), so no platform counts it twice. The
|
||||
// account exists already and the browser leaves for it next, so a
|
||||
// tag that throws here must not keep it on this page.
|
||||
try {
|
||||
track(analytics, EVENTS.SIGNUP_COMPLETED, { method: 'email' })
|
||||
} catch (err) {
|
||||
console.error('[event] signup_completed was not stated', err)
|
||||
}
|
||||
// The browser leaves for the app on the next line, so the whole
|
||||
// buffer goes now rather than waiting on the flush timer.
|
||||
analytics.flush(true)
|
||||
|
||||
Reference in New Issue
Block a user