first-referrer-cookie.test.ts439 lines · main
1import { describe, expect, it } from 'vitest'
2
3import {
4 buildFirstReferrerData,
5 FIRST_REFERRER_COOKIE_NAME,
6 hasPaidSignals,
7 isExternalReferrer,
8 isOAuthRedirectReferrer,
9 MW_DIAG_COOKIE_NAME,
10 parseFirstReferrerCookie,
11 parseMwDiagCookie,
12 serializeFirstReferrerCookie,
13 shouldRefreshCookie,
14} from './first-referrer-cookie'
15
16describe('first-referrer-cookie', () => {
17 describe('isExternalReferrer', () => {
18 it('returns false for briven domains', () => {
19 expect(isExternalReferrer('https://supabase.com')).toBe(false)
20 expect(isExternalReferrer('https://www.supabase.com')).toBe(false)
21 expect(isExternalReferrer('https://docs.supabase.com')).toBe(false)
22 })
23
24 it('returns true for external domains', () => {
25 expect(isExternalReferrer('https://google.com')).toBe(true)
26 expect(isExternalReferrer('https://chatgpt.com')).toBe(true)
27 })
28
29 it('returns true for http:// referrers', () => {
30 expect(isExternalReferrer('http://google.com')).toBe(true)
31 expect(isExternalReferrer('http://example.org/page')).toBe(true)
32 })
33
34 it('returns false for invalid values', () => {
35 expect(isExternalReferrer('')).toBe(false)
36 expect(isExternalReferrer('not-a-url')).toBe(false)
37 })
38 })
39
40 describe('isOAuthRedirectReferrer', () => {
41 // Google SSO — block entire domain
42 it('returns true for accounts.google.com (bare)', () => {
43 expect(isOAuthRedirectReferrer('https://accounts.google.com/')).toBe(true)
44 })
45
46 it('returns true for accounts.google.com with path', () => {
47 expect(
48 isOAuthRedirectReferrer('https://accounts.google.com/o/oauth2/auth?client_id=abc')
49 ).toBe(true)
50 })
51
52 // GitHub OAuth — block bare domain only
53 it('returns true for bare github.com/', () => {
54 expect(isOAuthRedirectReferrer('https://github.com/')).toBe(true)
55 })
56
57 it('returns true for bare github.com (no trailing slash)', () => {
58 expect(isOAuthRedirectReferrer('https://github.com')).toBe(true)
59 })
60
61 // GitHub genuine referrals — preserve these
62 it('returns false for github.com with repo path', () => {
63 expect(isOAuthRedirectReferrer('https://github.com/supabase/supabase')).toBe(false)
64 })
65
66 it('returns false for github.com with README path', () => {
67 expect(
68 isOAuthRedirectReferrer('https://github.com/supabase/supabase?tab=readme-ov-file')
69 ).toBe(false)
70 })
71
72 it('returns false for github.com with discussion path', () => {
73 expect(isOAuthRedirectReferrer('https://github.com/orgs/supabase/discussions/42949')).toBe(
74 false
75 )
76 })
77
78 it('returns false for github.com with blob path', () => {
79 expect(
80 isOAuthRedirectReferrer('https://github.com/supabase/supabase/blob/master/README.md')
81 ).toBe(false)
82 })
83
84 // GitHub OAuth explicit path (rare, but should still be caught)
85 it('returns true for github.com/login/oauth/authorize', () => {
86 expect(
87 isOAuthRedirectReferrer('https://github.com/login/oauth/authorize?client_id=abc')
88 ).toBe(true)
89 })
90
91 // Non-OAuth domains — should not match
92 it('returns false for google.com (search)', () => {
93 expect(isOAuthRedirectReferrer('https://www.google.com/')).toBe(false)
94 })
95
96 it('returns false for claude.ai', () => {
97 expect(isOAuthRedirectReferrer('https://claude.ai/')).toBe(false)
98 })
99
100 it('returns false for empty string', () => {
101 expect(isOAuthRedirectReferrer('')).toBe(false)
102 })
103
104 it('returns false for malformed URL', () => {
105 expect(isOAuthRedirectReferrer('not-a-url')).toBe(false)
106 })
107 })
108
109 describe('buildFirstReferrerData', () => {
110 it('handles malformed landing URL gracefully', () => {
111 const data = buildFirstReferrerData({
112 referrer: 'https://google.com',
113 landingUrl: 'not-a-valid-url',
114 })
115
116 expect(data.referrer).toBe('https://google.com')
117 expect(data.landing_url).toBe('not-a-valid-url')
118 expect(data.utms).toEqual({})
119 expect(data.click_ids).toEqual({})
120 })
121
122 it('extracts utm and click-id params from landing url', () => {
123 const data = buildFirstReferrerData({
124 referrer: 'https://www.google.com/',
125 landingUrl:
126 'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456',
127 })
128
129 expect(data.referrer).toBe('https://www.google.com/')
130 expect(data.landing_url).toBe(
131 'https://supabase.com/pricing?utm_source=google&utm_medium=cpc&utm_campaign=test&gclid=abc123&msclkid=xyz456'
132 )
133
134 expect(data.utms).toEqual({
135 utm_source: 'google',
136 utm_medium: 'cpc',
137 utm_campaign: 'test',
138 })
139
140 expect(data.click_ids).toEqual({
141 gclid: 'abc123',
142 msclkid: 'xyz456',
143 })
144 })
145 })
146
147 describe('serialize / parse', () => {
148 it('round-trips valid cookie payloads', () => {
149 const input = buildFirstReferrerData({
150 referrer: 'https://www.google.com/',
151 landingUrl: 'https://supabase.com/pricing?utm_source=google',
152 })
153
154 const encoded = serializeFirstReferrerCookie(input)
155 const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)
156
157 expect(parsed).toEqual(input)
158 })
159
160 it('returns null for empty string', () => {
161 expect(parseFirstReferrerCookie('')).toBeNull()
162 })
163
164 it('parses cookie from header with multiple cookies', () => {
165 const input = buildFirstReferrerData({
166 referrer: 'https://google.com/',
167 landingUrl: 'https://supabase.com/',
168 })
169 const encoded = serializeFirstReferrerCookie(input)
170 const header = `session=abc123; ${FIRST_REFERRER_COOKIE_NAME}=${encoded}; theme=dark`
171
172 expect(parseFirstReferrerCookie(header)).toEqual(input)
173 })
174
175 it('returns null for malformed json', () => {
176 expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=%7Bnot-json`)).toBeNull()
177 })
178
179 it('returns null for invalid payload shape', () => {
180 const encoded = encodeURIComponent(JSON.stringify({ foo: 'bar' }))
181 expect(parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)).toBeNull()
182 })
183
184 it('parses double-encoded cookies (legacy format before serializer fix)', () => {
185 const input = buildFirstReferrerData({
186 referrer: 'https://www.google.com/',
187 landingUrl: 'https://supabase.com/pricing?utm_source=google',
188 })
189
190 // Simulate the old double-encoding: encodeURIComponent(JSON.stringify(data))
191 // followed by Next.js cookies.set() encoding it again.
192 const doubleEncoded = encodeURIComponent(encodeURIComponent(JSON.stringify(input)))
193 const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${doubleEncoded}`)
194
195 expect(parsed).toEqual(input)
196 })
197
198 it('drops non-string values in utms/click_ids', () => {
199 const encoded = encodeURIComponent(
200 JSON.stringify({
201 referrer: 'https://www.google.com/',
202 landing_url: 'https://supabase.com/pricing',
203 utms: { utm_source: 'google', utm_medium: 123 },
204 click_ids: { gclid: 'abc', msclkid: null },
205 ts: 123,
206 })
207 )
208
209 const parsed = parseFirstReferrerCookie(`${FIRST_REFERRER_COOKIE_NAME}=${encoded}`)
210
211 expect(parsed).toEqual({
212 referrer: 'https://www.google.com/',
213 landing_url: 'https://supabase.com/pricing',
214 utms: { utm_source: 'google' },
215 click_ids: { gclid: 'abc' },
216 ts: 123,
217 })
218 })
219 })
220
221 describe('hasPaidSignals', () => {
222 it('detects click IDs', () => {
223 expect(hasPaidSignals(new URL('https://supabase.com/?gclid=abc'))).toBe(true)
224 expect(hasPaidSignals(new URL('https://supabase.com/?fbclid=abc'))).toBe(true)
225 expect(hasPaidSignals(new URL('https://supabase.com/?msclkid=abc'))).toBe(true)
226 expect(hasPaidSignals(new URL('https://supabase.com/?gbraid=abc'))).toBe(true)
227 expect(hasPaidSignals(new URL('https://supabase.com/?wbraid=abc'))).toBe(true)
228 expect(hasPaidSignals(new URL('https://supabase.com/?rdt_cid=abc'))).toBe(true)
229 expect(hasPaidSignals(new URL('https://supabase.com/?ttclid=abc'))).toBe(true)
230 expect(hasPaidSignals(new URL('https://supabase.com/?twclid=abc'))).toBe(true)
231 expect(hasPaidSignals(new URL('https://supabase.com/?li_fat_id=abc'))).toBe(true)
232 })
233
234 it('detects paid utm_medium values', () => {
235 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=cpc'))).toBe(true)
236 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=ppc'))).toBe(true)
237 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_search'))).toBe(true)
238 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paidsocial'))).toBe(true)
239 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=paid_social'))).toBe(true)
240 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=display'))).toBe(true)
241 })
242
243 it('is case-insensitive for utm_medium', () => {
244 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=CPC'))).toBe(true)
245 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=Paid_Search'))).toBe(true)
246 })
247
248 it('returns false for organic traffic', () => {
249 expect(hasPaidSignals(new URL('https://supabase.com/'))).toBe(false)
250 expect(hasPaidSignals(new URL('https://supabase.com/?utm_source=google'))).toBe(false)
251 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=email'))).toBe(false)
252 expect(hasPaidSignals(new URL('https://supabase.com/?utm_medium=organic'))).toBe(false)
253 })
254 })
255
256 describe('parseMwDiagCookie', () => {
257 it('parses well-formed value with would_stamp=1 and has_cookie=0', () => {
258 const header = `${MW_DIAG_COOKIE_NAME}=hit=1&would_stamp=1&has_cookie=0`
259 expect(parseMwDiagCookie(header)).toEqual({
260 hit: true,
261 would_stamp: true,
262 has_existing_cookie: false,
263 })
264 })
265
266 it('parses well-formed value with would_stamp=0 and has_cookie=1', () => {
267 const header = `${MW_DIAG_COOKIE_NAME}=hit=1&would_stamp=0&has_cookie=1`
268 expect(parseMwDiagCookie(header)).toEqual({
269 hit: true,
270 would_stamp: false,
271 has_existing_cookie: true,
272 })
273 })
274
275 it('returns null when hit=0', () => {
276 const header = `${MW_DIAG_COOKIE_NAME}=hit=0&would_stamp=1&has_cookie=1`
277 expect(parseMwDiagCookie(header)).toBeNull()
278 })
279
280 it('returns object with would_stamp: false when would_stamp key is missing', () => {
281 const header = `${MW_DIAG_COOKIE_NAME}=hit=1&has_cookie=1`
282 expect(parseMwDiagCookie(header)).toEqual({
283 hit: true,
284 would_stamp: false,
285 has_existing_cookie: true,
286 })
287 })
288
289 it('returns object with has_existing_cookie: false when has_cookie key is missing', () => {
290 const header = `${MW_DIAG_COOKIE_NAME}=hit=1&would_stamp=1`
291 expect(parseMwDiagCookie(header)).toEqual({
292 hit: true,
293 would_stamp: true,
294 has_existing_cookie: false,
295 })
296 })
297
298 it('returns null for empty string input', () => {
299 expect(parseMwDiagCookie('')).toBeNull()
300 })
301
302 it('returns null when cookie is not present in header', () => {
303 expect(parseMwDiagCookie('session=abc123; theme=dark')).toBeNull()
304 })
305
306 it('returns null for garbage value', () => {
307 const header = `${MW_DIAG_COOKIE_NAME}=not-a-valid-query-string`
308 expect(parseMwDiagCookie(header)).toBeNull()
309 })
310
311 it('parses correct cookie from header with multiple cookies', () => {
312 const header = `session=abc123; ${MW_DIAG_COOKIE_NAME}=hit=1&would_stamp=1&has_cookie=0; theme=dark`
313 expect(parseMwDiagCookie(header)).toEqual({
314 hit: true,
315 would_stamp: true,
316 has_existing_cookie: false,
317 })
318 })
319
320 it('parses URL-encoded value from Next.js response.cookies.set()', () => {
321 const header = `${MW_DIAG_COOKIE_NAME}=hit%3D1%26would_stamp%3D1%26has_cookie%3D0`
322 expect(parseMwDiagCookie(header)).toEqual({
323 hit: true,
324 would_stamp: true,
325 has_existing_cookie: false,
326 })
327 })
328
329 it('parses URL-encoded value with has_cookie=1', () => {
330 const header = `${MW_DIAG_COOKIE_NAME}=hit%3D1%26would_stamp%3D0%26has_cookie%3D1`
331 expect(parseMwDiagCookie(header)).toEqual({
332 hit: true,
333 would_stamp: false,
334 has_existing_cookie: true,
335 })
336 })
337
338 it('parses URL-encoded value among multiple cookies', () => {
339 const header = `session=abc; ${MW_DIAG_COOKIE_NAME}=hit%3D1%26would_stamp%3D0%26has_cookie%3D0; theme=dark`
340 expect(parseMwDiagCookie(header)).toEqual({
341 hit: true,
342 would_stamp: false,
343 has_existing_cookie: false,
344 })
345 })
346 })
347
348 describe('shouldRefreshCookie', () => {
349 it('stamps when no cookie and external referrer', () => {
350 expect(
351 shouldRefreshCookie(false, {
352 referrer: 'https://google.com',
353 url: 'https://supabase.com/',
354 })
355 ).toEqual({ stamp: true })
356 })
357
358 it('skips when no cookie and internal referrer', () => {
359 expect(
360 shouldRefreshCookie(false, {
361 referrer: 'https://supabase.com/docs',
362 url: 'https://supabase.com/dashboard',
363 })
364 ).toEqual({ stamp: false })
365 })
366
367 it('skips when cookie exists and no paid signals', () => {
368 expect(
369 shouldRefreshCookie(true, {
370 referrer: 'https://google.com',
371 url: 'https://supabase.com/',
372 })
373 ).toEqual({ stamp: false })
374 })
375
376 it('refreshes when cookie exists but URL has paid signals', () => {
377 expect(
378 shouldRefreshCookie(true, {
379 referrer: 'https://google.com',
380 url: 'https://supabase.com/?gclid=abc123',
381 })
382 ).toEqual({ stamp: true })
383
384 expect(
385 shouldRefreshCookie(true, {
386 referrer: 'https://google.com',
387 url: 'https://supabase.com/?utm_medium=cpc&utm_source=google',
388 })
389 ).toEqual({ stamp: true })
390 })
391
392 it('skips when no cookie and no referrer (direct navigation)', () => {
393 expect(shouldRefreshCookie(false, { referrer: '', url: 'https://supabase.com/' })).toEqual({
394 stamp: false,
395 })
396 })
397
398 it('handles malformed URL gracefully', () => {
399 expect(
400 shouldRefreshCookie(true, {
401 referrer: 'https://google.com',
402 url: 'not-a-valid-url',
403 })
404 ).toEqual({ stamp: false })
405 })
406
407 it('does not stamp for GitHub OAuth redirect (bare domain)', () => {
408 const result = shouldRefreshCookie(false, {
409 referrer: 'https://github.com/',
410 url: 'https://supabase.com/dashboard',
411 })
412 expect(result.stamp).toBe(false)
413 })
414
415 it('does not stamp for Google SSO redirect', () => {
416 const result = shouldRefreshCookie(false, {
417 referrer: 'https://accounts.google.com/',
418 url: 'https://supabase.com/dashboard',
419 })
420 expect(result.stamp).toBe(false)
421 })
422
423 it('still stamps for genuine GitHub referral with path', () => {
424 const result = shouldRefreshCookie(false, {
425 referrer: 'https://github.com/supabase/supabase?tab=readme-ov-file',
426 url: 'https://supabase.com/',
427 })
428 expect(result.stamp).toBe(true)
429 })
430
431 it('still re-stamps existing cookie for paid signals regardless of OAuth referrer', () => {
432 const result = shouldRefreshCookie(true, {
433 referrer: 'https://github.com/',
434 url: 'https://supabase.com/pricing?gclid=abc123',
435 })
436 expect(result.stamp).toBe(true)
437 })
438 })
439})