mirror of
https://github.com/supabase/supabase.git
synced 2026-05-09 02:09:50 -04:00
334 lines
12 KiB
TypeScript
334 lines
12 KiB
TypeScript
import { FIRST_REFERRER_COOKIE_NAME } from 'common/first-referrer-cookie'
|
|
import { NextRequest } from 'next/server'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { middleware } from './middleware'
|
|
|
|
// content.generated.ts is produced by scripts/generateMdContent.mjs at
|
|
// content:build time and gitignored, so it isn't on disk in CI before tests
|
|
// run. The mock seeds a representative allowlist so the .md-routing branches
|
|
// are actually exercised below.
|
|
vi.mock('./app/api-v2/md/content.generated', () => ({
|
|
MD_CONTENT: new Map<string, string>(),
|
|
MD_PAGES: new Set<string>(['homepage', 'auth', 'pricing']),
|
|
}))
|
|
|
|
function makeRequest(
|
|
url: string,
|
|
{
|
|
referer,
|
|
hasCookie,
|
|
accept,
|
|
userAgent,
|
|
}: { referer?: string; hasCookie?: boolean; accept?: string; userAgent?: string } = {}
|
|
): NextRequest {
|
|
const headers: Record<string, string> = {}
|
|
if (referer) headers.referer = referer
|
|
if (accept) headers.accept = accept
|
|
if (userAgent) headers['user-agent'] = userAgent
|
|
const req = new NextRequest(new URL(url, 'https://supabase.com'), { headers })
|
|
if (hasCookie) {
|
|
req.cookies.set(FIRST_REFERRER_COOKIE_NAME, 'existing')
|
|
}
|
|
return req
|
|
}
|
|
|
|
describe('www middleware', () => {
|
|
describe('cookie stamping on www paths', () => {
|
|
it('stamps cookie for external referrer on www path', () => {
|
|
const req = makeRequest('/pricing', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeDefined()
|
|
})
|
|
|
|
it('does not stamp cookie for internal referrer', () => {
|
|
const req = makeRequest('/pricing', { referer: 'https://supabase.com/docs' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('cookie stamping on /dashboard paths', () => {
|
|
it('stamps cookie for external referrer', () => {
|
|
const req = makeRequest('/dashboard/project/123', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeDefined()
|
|
})
|
|
|
|
it('does not stamp cookie for internal referrer', () => {
|
|
const req = makeRequest('/dashboard/project/123', {
|
|
referer: 'https://supabase.com/pricing',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
|
|
it('does not stamp cookie for direct navigation (no referrer)', () => {
|
|
const req = makeRequest('/dashboard/project/123')
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('cookie stamping on /docs paths', () => {
|
|
it('stamps cookie for external referrer', () => {
|
|
const req = makeRequest('/docs/guides/auth', { referer: 'https://google.com' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeDefined()
|
|
})
|
|
|
|
it('does not stamp cookie for internal referrer', () => {
|
|
const req = makeRequest('/docs/guides/auth', {
|
|
referer: 'https://supabase.com/pricing',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
|
|
it('does not stamp cookie for direct navigation (no referrer)', () => {
|
|
const req = makeRequest('/docs/guides/auth')
|
|
const res = middleware(req)
|
|
|
|
expect(res.cookies.get(FIRST_REFERRER_COOKIE_NAME)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('.md suffix routing', () => {
|
|
it('rewrites /<slug>.md for allowlisted slugs', () => {
|
|
const req = makeRequest('/auth.md')
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('falls through for non-allowlisted .md slugs', () => {
|
|
const req = makeRequest('/not-a-page.md')
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Accept: text/markdown content negotiation', () => {
|
|
it('rewrites / to homepage when Accept: text/markdown', () => {
|
|
const req = makeRequest('/', { accept: 'text/markdown' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe(
|
|
'https://supabase.com/api-v2/md/homepage'
|
|
)
|
|
})
|
|
|
|
it('rewrites /<slug> when Accept: text/markdown matches the allowlist', () => {
|
|
const req = makeRequest('/auth', { accept: 'text/markdown' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('rewrites /<slug>/ (trailing slash) the same as /<slug>', () => {
|
|
const req = makeRequest('/auth/', { accept: 'text/markdown' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('falls through when Accept does not include text/markdown', () => {
|
|
const req = makeRequest('/auth', { accept: 'text/html' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
|
|
it('falls through when slug is not in the allowlist', () => {
|
|
const req = makeRequest('/not-a-page', { accept: 'text/markdown' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Accept header q-value parsing', () => {
|
|
it('serves markdown for Cursor-style Accept (markdown preferred, plain fallback)', () => {
|
|
const req = makeRequest('/auth', {
|
|
accept: 'text/markdown, text/plain;q=0.9, */*;q=0.8',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('serves markdown when md and html have equal q-values', () => {
|
|
const req = makeRequest('/auth', { accept: 'text/markdown, text/html, */*' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('serves HTML when html q-value beats markdown q-value', () => {
|
|
const req = makeRequest('/auth', { accept: 'text/html;q=1.0, text/markdown;q=0.5' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
|
|
it('serves HTML for browser-style Accept (html with */* fallback)', () => {
|
|
const req = makeRequest('/auth', {
|
|
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
|
|
it('serves markdown when md q-value beats html q-value', () => {
|
|
const req = makeRequest('/auth', { accept: 'text/html;q=0.5, text/markdown;q=1.0' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('tolerates OWS around the q parameter (per RFC 9110)', () => {
|
|
const req = makeRequest('/auth', { accept: 'text/html ; q = 1.0, text/markdown ; q = 0.5' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
|
|
it('ignores out-of-range q-values rather than treating them as preference', () => {
|
|
const req = makeRequest('/auth', { accept: 'text/html;q=2.0, text/markdown;q=1.0' })
|
|
const res = middleware(req)
|
|
|
|
// text/html's q=2.0 is invalid and falls back to default 1.0; tie -> markdown.
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
})
|
|
|
|
describe('406 Not Acceptable', () => {
|
|
it('returns 406 on MD-eligible page when Accept excludes every type we serve', () => {
|
|
const req = makeRequest('/pricing', { accept: 'application/x-content-negotiation-probe' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.status).toBe(406)
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
|
|
it('does not return 406 on non-MD pages (no negotiation contract there)', () => {
|
|
const req = makeRequest('/not-a-page', { accept: 'application/x-content-negotiation-probe' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.status).not.toBe(406)
|
|
})
|
|
|
|
it('does not return 406 when Accept includes */*', () => {
|
|
const req = makeRequest('/pricing', { accept: '*/*' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.status).not.toBe(406)
|
|
})
|
|
|
|
it('does not return 406 for LLM UAs even with a probe Accept header', () => {
|
|
const req = makeRequest('/pricing', {
|
|
accept: 'application/x-content-negotiation-probe',
|
|
userAgent: 'Claude-User/1.0',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.status).not.toBe(406)
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/pricing')
|
|
})
|
|
|
|
it('returns 406 on changelog entries when Accept excludes every type', () => {
|
|
const req = makeRequest('/changelog/100', {
|
|
accept: 'application/x-content-negotiation-probe',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.status).toBe(406)
|
|
})
|
|
|
|
it('sets Cache-Control: no-store and Vary: Accept on 406 responses', () => {
|
|
const req = makeRequest('/pricing', { accept: 'application/x-content-negotiation-probe' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.status).toBe(406)
|
|
expect(res.headers.get('Cache-Control')).toBe('no-store')
|
|
expect(res.headers.get('Vary')).toBe('Accept')
|
|
})
|
|
})
|
|
|
|
describe('LLM user-agent routing', () => {
|
|
it('rewrites for Claude-User', () => {
|
|
const req = makeRequest('/pricing', {
|
|
userAgent: 'Claude-User (claude-code/2.1.119; +https://support.anthropic.com/)',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/pricing')
|
|
})
|
|
|
|
it('rewrites for ChatGPT-User', () => {
|
|
const req = makeRequest('/auth', { userAgent: 'Mozilla/5.0 (compatible; ChatGPT-User/1.0)' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('rewrites for Claude-Web', () => {
|
|
const req = makeRequest('/pricing', { userAgent: 'Claude-Web/1.0' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/pricing')
|
|
})
|
|
|
|
it('rewrites for PerplexityBot', () => {
|
|
const req = makeRequest('/auth/', { userAgent: 'PerplexityBot/1.0' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBe('https://supabase.com/api-v2/md/auth')
|
|
})
|
|
|
|
it('falls through for UAs that embed a match as a substring', () => {
|
|
for (const ua of ['chatgpt-userscript/2.0', 'NotPerplexityBot']) {
|
|
const req = makeRequest('/auth', { userAgent: ua })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
}
|
|
})
|
|
|
|
it('falls through for browser user agents', () => {
|
|
const req = makeRequest('/auth', {
|
|
userAgent:
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
|
})
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
|
|
it('falls through for training crawlers (GPTBot, ClaudeBot, CCBot)', () => {
|
|
for (const ua of ['GPTBot/1.0', 'ClaudeBot/1.0', 'CCBot/2.0']) {
|
|
const req = makeRequest('/auth', { userAgent: ua })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
}
|
|
})
|
|
|
|
it('falls through when slug is not in the allowlist', () => {
|
|
const req = makeRequest('/not-a-page', { userAgent: 'Claude-User' })
|
|
const res = middleware(req)
|
|
|
|
expect(res.headers.get('x-middleware-rewrite')).toBeNull()
|
|
})
|
|
})
|
|
})
|