Files
what-to-eat/test/acquisition.test.ts
T

159 lines
7.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
function browser(url = 'https://cook.yunle.fun/?utm_source=知乎&utm_medium=social&utm_campaign=秋季菜谱', referrer = 'https://www.baidu.com/search?q=private') {
const local = new Map<string, string>()
const session = new Map<string, string>()
const storage = (values: Map<string, string>) => ({
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => values.set(key, value),
})
return {
document: { referrer },
localStorage: storage(local),
location: new URL(url),
sessionStorage: storage(session),
} as unknown as Pick<Window, 'location' | 'document' | 'localStorage' | 'sessionStorage'>
}
beforeEach(() => vi.resetModules())
describe('cook document acquisition', () => {
it.each([
'https://yunle.fun/',
'https://www.yunle.fun/login',
'https://apps.yunle.fun/',
'https://nested.apps.yunle.fun/',
'https://cook.yunyoujun.cn/',
'https://yunle.fun./login',
'https://cook.yunle.fun/random',
])('excludes internal ecosystem navigation from external acquisition: %s', async (referrer) => {
const { captureCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const context = captureCookAcquisition(browser('https://cook.yunle.fun/', referrer), 1000)!
expect(context.firstTouch.referrerHost).toBe('')
expect(context.registerTouch.referrerHost).toBe('')
})
it.each(['https://github.com/YunYouJun/cook', 'https://www.zhihu.com/', 'https://notyunle.fun/'])(
'retains an actual external referrer: %s',
async (referrer) => {
const { captureCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const context = captureCookAcquisition(browser('https://cook.yunle.fun/', referrer), 1000)!
expect(context.registerTouch.referrerHost).toBe(new URL(referrer).hostname)
},
)
it('keeps first touch for 30 days while updating the current document entry', async () => {
const { captureCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const entry = browser()
const first = captureCookAcquisition(entry, 1000)!
entry.location = new URL('https://cook.yunle.fun/random?utm_source=github') as unknown as Location
const second = captureCookAcquisition(entry, 2000)!
expect(second.firstTouch).toEqual(first.firstTouch)
expect(second.registerTouch.landingPath).toBe('/random')
expect(second.registerTouch.utmSource).toBe('github')
expect(second.registerTouch.referrerHost).toBe('www.baidu.com')
expect(JSON.stringify(second)).not.toContain('q=private')
const expired = captureCookAcquisition(entry, 1000 + 30 * 86400000)!
expect(expired.firstTouch).toEqual(expired.registerTouch)
expect(expired.firstTouch.utmSource).toBe('github')
})
it('preserves the pre-auth entry through a callback and never captures OAuth referrers', async () => {
const { captureCookAcquisition, readCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const entry = browser()
const context = captureCookAcquisition(entry, 1000)
entry.location = new URL('https://cook.yunle.fun/auth/callback?code=private#ylf_sso=private') as unknown as Location
Object.assign(entry.document, { referrer: 'https://github.com/login/oauth' })
expect(captureCookAcquisition(entry, 2000)).toEqual(context)
expect(readCookAcquisition(entry, 2000)).toEqual(context)
expect(JSON.stringify(context)).not.toContain('private')
})
it('does not invent attribution for a callback without an earlier document entry', async () => {
const { captureCookAcquisition } = await import('../apps/web/app/utils/acquisition')
expect(captureCookAcquisition(browser('https://cook.yunle.fun/auth/callback'), 1000)).toBeNull()
})
it.each([null, '{broken', JSON.stringify({ capturedAt: -1 })])('does not restore a readable deleted or invalid entry from memory: %s', async (stored) => {
const { captureCookAcquisition, readCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const entry = browser()
expect(captureCookAcquisition(entry, 1000)?.registerTouch.utmSource).toBe('知乎')
entry.sessionStorage.getItem = () => stored
expect(readCookAcquisition(entry, 2000)).toBeNull()
entry.sessionStorage.getItem = () => {
throw new Error('storage blocked')
}
expect(readCookAcquisition(entry, 3000)).toBeNull()
})
it('uses the current entry when readable first touch is cleared, without reviving its old campaign', async () => {
const { captureCookAcquisition, readCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const entry = browser()
captureCookAcquisition(entry, 1000)
entry.location = new URL('https://cook.yunle.fun/random?utm_source=github') as unknown as Location
captureCookAcquisition(entry, 2000)
entry.localStorage.getItem = () => null
expect(readCookAcquisition(entry, 3000)?.firstTouch.utmSource).toBe('github')
entry.localStorage.getItem = () => {
throw new Error('storage blocked')
}
expect(readCookAcquisition(entry, 4000)?.firstTouch.utmSource).toBe('github')
})
it('keeps a validated memory first touch across blocked storage and expires stale entry data', async () => {
const { captureCookAcquisition, readCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const entry = browser()
for (const storage of [entry.localStorage, entry.sessionStorage]) {
storage.getItem = () => {
throw new Error('storage blocked')
}
storage.setItem = () => {
throw new Error('storage blocked')
}
}
captureCookAcquisition(entry, 1000)
entry.location = new URL('https://cook.yunle.fun/random?utm_source=github') as unknown as Location
const current = captureCookAcquisition(entry, 2000)!
expect(current.firstTouch.utmSource).toBe('知乎')
expect(current.registerTouch.utmSource).toBe('github')
expect(readCookAcquisition(entry, 2000 + 30 * 86400000)).toBeNull()
})
it('uses the new entry when a failed write leaves the old value readable, then respects deletion', async () => {
const { captureCookAcquisition, readCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const entry = browser('https://cook.yunle.fun/?utm_source=old')
captureCookAcquisition(entry, 1000)
entry.sessionStorage.setItem = () => {
throw new Error('quota exceeded')
}
entry.location = new URL('https://cook.yunle.fun/random?utm_source=new') as unknown as Location
expect(captureCookAcquisition(entry, 2000)?.registerTouch.utmSource).toBe('new')
expect(readCookAcquisition(entry, 3000)?.registerTouch.utmSource).toBe('new')
expect(readCookAcquisition(entry, 3000)?.firstTouch.utmSource).toBe('old')
entry.sessionStorage.getItem = () => null
expect(readCookAcquisition(entry, 4000)).toBeNull()
entry.sessionStorage.getItem = () => {
throw new Error('storage blocked')
}
expect(readCookAcquisition(entry, 5000)).toBeNull()
})
it('ignores credentials, invalid UTM and storage failures without blocking authentication', async () => {
const { captureCookAcquisition, readCookAcquisition } = await import('../apps/web/app/utils/acquisition')
const entry = browser('https://cook.yunle.fun/?utm_source=hello%0Aworld&token=private', 'https://user:pass@example.com/path')
for (const storage of ['localStorage', 'sessionStorage']) {
Object.defineProperty(entry, storage, {
get() {
throw new Error('storage blocked')
},
})
}
const context = captureCookAcquisition(entry, 1000)!
expect(context.registerTouch.referrerHost).toBe('')
expect(context.registerTouch.utmSource).toBe('')
expect(readCookAcquisition(entry, 2000)).toEqual(context)
expect(JSON.stringify(context)).not.toContain('private')
})
})