import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
decodeFirstTouch,
encodeFirstTouch,
generateAnonymousId,
isAcquisitionChannel,
isValidAnonymousId,
readAnonymousIdCookie,
readFirstTouchCookie,
signAnonymousId,
signFirstTouch,
type FirstTouch,
} from '@/lib/analytics/cookies';
import { isCountableDocumentRequest, isLikelyBot } from '@/lib/analytics/bots';
beforeEach(() => {
vi.stubEnv('NEXTAUTH_SECRET', 'cookie-test-secret');
});
afterEach(() => {
vi.unstubAllEnvs();
});
/** The cookie body a forged value would have to carry, in the wire form the reader expects. */
function body(payload: unknown): string {
return btoa(JSON.stringify(payload)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const TOUCH: FirstTouch = {
channel: 'GITHUB',
utmSource: 'github',
utmMedium: 'readme',
utmCampaign: 'launch',
referrerHost: 'github.com',
landingPath: '/vs/frameio',
};
describe('first touch cookie', () => {
it('round-trips every field', () => {
expect(decodeFirstTouch(encodeFirstTouch(TOUCH))).toEqual(TOUCH);
});
it('round-trips a touch with nothing but a channel', () => {
const bare: FirstTouch = {
channel: 'DIRECT',
utmSource: null,
utmMedium: null,
utmCampaign: null,
referrerHost: null,
landingPath: '/',
};
expect(decodeFirstTouch(encodeFirstTouch(bare))).toEqual(bare);
});
it('rejects a hand-edited cookie carrying an unknown channel', () => {
expect(decodeFirstTouch(body({ c: 'INVESTOR_DEMO', p: '/' }))).toBeNull();
});
it('re-sanitizes fields rather than trusting the cookie', () => {
const decoded = decodeFirstTouch(
body({ c: 'DIRECT', p: '/x', s: '', r: 'not a host' })
);
expect(decoded?.utmSource).toBeNull();
expect(decoded?.referrerHost).toBeNull();
});
it('re-sanitizes a landing path that could only have been hand-written', () => {
expect(decodeFirstTouch(body({ c: 'DIRECT', p: '/
' }))?.landingPath).toBe(
'/'
);
});
it('returns null for garbage and for an absent cookie', () => {
expect(decodeFirstTouch('%%%not-base64%%%')).toBeNull();
expect(decodeFirstTouch(null)).toBeNull();
expect(decodeFirstTouch(body(['DIRECT']))).toBeNull();
});
});
describe('anonymous id', () => {
it('generates an id the validator accepts', () => {
expect(isValidAnonymousId(generateAnonymousId())).toBe(true);
});
it('generates a different id each time', () => {
expect(generateAnonymousId()).not.toBe(generateAnonymousId());
});
it('rejects an id that is too short, too long or not base36', () => {
expect(isValidAnonymousId('abc')).toBe(false);
expect(isValidAnonymousId('a'.repeat(65))).toBe(false);
expect(isValidAnonymousId('ABCDEF0123456789ABCD')).toBe(false);
expect(isValidAnonymousId(undefined)).toBe(false);
});
});
// Everything above tests the unsigned inner layer. Nothing outside the module
// uses it: a cookie is only a visitor once the signature says this deployment
// issued it, which is what stops a caller from inventing one with curl.
describe('signed cookies', () => {
const TOUCH_TO_SIGN: FirstTouch = {
channel: 'YOUTUBE',
utmSource: 'yt',
utmMedium: null,
utmCampaign: null,
referrerHost: 'youtube.com',
landingPath: '/',
};
it('round-trips an id and a first touch', async () => {
const id = generateAnonymousId();
expect(await readAnonymousIdCookie(await signAnonymousId(id))).toBe(id);
expect(await readFirstTouchCookie(await signFirstTouch(TOUCH_TO_SIGN))).toEqual(TOUCH_TO_SIGN);
});
it('rejects a well-formed id that carries no signature', async () => {
expect(await readAnonymousIdCookie('a1b2c3d4e5f60718293a4b5c6d7e8f90')).toBeNull();
});
it('rejects a value whose body was edited under a valid signature', async () => {
const signed = (await signAnonymousId(generateAnonymousId())) ?? '';
const [mac] = signed.split('.');
expect(await readAnonymousIdCookie(`${mac}.a1b2c3d4e5f60718293a4b5c6d7e8f90`)).toBeNull();
});
it('rejects a first touch re-signed to name another channel', async () => {
const forgedBody = encodeFirstTouch({ ...TOUCH_TO_SIGN, channel: 'GITHUB' });
const signed = (await signFirstTouch(TOUCH_TO_SIGN)) ?? '';
const [mac] = signed.split('.');
expect(await readFirstTouchCookie(`${mac}.${forgedBody}`)).toBeNull();
});
it('rejects a cookie signed with another deployment key', async () => {
const signed = await signAnonymousId(generateAnonymousId());
vi.stubEnv('NEXTAUTH_SECRET', 'someone-elses-secret');
expect(await readAnonymousIdCookie(signed)).toBeNull();
});
it('signs nothing and accepts nothing when there is no secret', async () => {
const signed = await signAnonymousId(generateAnonymousId());
vi.stubEnv('NEXTAUTH_SECRET', undefined);
vi.stubEnv('AUTH_SECRET', undefined);
expect(await signAnonymousId(generateAnonymousId())).toBeNull();
expect(await readAnonymousIdCookie(signed)).toBeNull();
});
it('rejects the empty, the truncated and the separator-less', async () => {
expect(await readAnonymousIdCookie('')).toBeNull();
expect(await readAnonymousIdCookie(undefined)).toBeNull();
expect(await readAnonymousIdCookie('.')).toBeNull();
expect(await readAnonymousIdCookie('a'.repeat(22))).toBeNull();
expect(await readFirstTouchCookie('not-signed-at-all')).toBeNull();
});
});
describe('isAcquisitionChannel', () => {
it('accepts the nine buckets and nothing else', () => {
expect(isAcquisitionChannel('REVIEW_LINK')).toBe(true);
expect(isAcquisitionChannel('direct')).toBe(false);
expect(isAcquisitionChannel(7)).toBe(false);
});
});
describe('isLikelyBot', () => {
it('passes a real browser through', () => {
expect(
isLikelyBot(
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36'
)
).toBe(false);
});
it('catches crawlers, link previewers and scripts', () => {
expect(isLikelyBot('Googlebot/2.1 (+http://www.google.com/bot.html)')).toBe(true);
expect(isLikelyBot('facebookexternalhit/1.1')).toBe(true);
expect(isLikelyBot('curl/8.4.0')).toBe(true);
expect(isLikelyBot('python-requests/2.31.0')).toBe(true);
expect(isLikelyBot('HeadlessChrome/120.0.0.0')).toBe(true);
});
it('treats a missing user agent as a bot', () => {
expect(isLikelyBot('')).toBe(true);
expect(isLikelyBot(null)).toBe(true);
});
});
describe('isCountableDocumentRequest', () => {
it('counts a real page load', () => {
expect(isCountableDocumentRequest(new Headers({ 'sec-fetch-dest': 'document' }))).toBe(true);
});
it('does not count a prefetch of the register page', () => {
expect(
isCountableDocumentRequest(
new Headers({ 'sec-fetch-dest': 'document', 'sec-purpose': 'prefetch;prerender' })
)
).toBe(false);
expect(
isCountableDocumentRequest(
new Headers({ 'sec-fetch-dest': 'document', 'next-router-prefetch': '1' })
)
).toBe(false);
});
it('does not count an RSC navigation or a subresource', () => {
expect(
isCountableDocumentRequest(new Headers({ 'sec-fetch-dest': 'document', rsc: '1' }))
).toBe(false);
expect(isCountableDocumentRequest(new Headers({ 'sec-fetch-dest': 'image' }))).toBe(false);
});
it('falls back to the accept header when fetch metadata is missing', () => {
expect(isCountableDocumentRequest(new Headers({ accept: 'text/html,*/*' }))).toBe(true);
expect(isCountableDocumentRequest(new Headers({ accept: 'application/json' }))).toBe(false);
expect(isCountableDocumentRequest(new Headers())).toBe(false);
});
});