a83baede72
* fix: block staging backend on production brands * fix: clarify production domain classification * fix: alert on forbidden white-label backend
105 lines
3.5 KiB
TypeScript
105 lines
3.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server'
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
const updateSessionMock = vi.hoisted(() =>
|
|
vi.fn(async () => new NextResponse(null, { status: 204 })),
|
|
)
|
|
const loggerErrorMock = vi.hoisted(() => vi.fn())
|
|
|
|
vi.mock('@/lib/supabase/middleware', () => ({
|
|
updateSession: updateSessionMock,
|
|
}))
|
|
vi.mock('@/lib/logger', () => ({
|
|
createLogger: () => ({ error: loggerErrorMock }),
|
|
}))
|
|
|
|
import { config, proxy } from './proxy'
|
|
|
|
const STAGING_URL = 'https://metjnjrhvujscngnpzdv.supabase.co'
|
|
const PRODUCTION_URL = 'https://pwxtzglxptnnvjrpixpg.supabase.co'
|
|
|
|
describe('production white-label proxy guard', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.stubEnv('NEXT_PUBLIC_SUPABASE_URL', STAGING_URL)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it.each(['/login', '/auth/callback', '/api/health'])(
|
|
'covers the application request path %s',
|
|
pathname => {
|
|
expect(
|
|
unstable_doesMiddlewareMatch({
|
|
config,
|
|
nextConfig: {},
|
|
url: `https://acount.accounted.se${pathname}`,
|
|
}),
|
|
).toBe(true)
|
|
},
|
|
)
|
|
|
|
it('returns a non-cacheable empty 503 before session handling', async () => {
|
|
const response = await proxy(
|
|
new NextRequest('https://acount.accounted.se/login'),
|
|
)
|
|
|
|
expect(response.status).toBe(503)
|
|
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
expect(await response.text()).toBe('')
|
|
expect(updateSessionMock).not.toHaveBeenCalled()
|
|
expect(loggerErrorMock).toHaveBeenCalledOnce()
|
|
expect(loggerErrorMock).toHaveBeenCalledWith(
|
|
'Blocked production white-label host from staging backend',
|
|
{
|
|
alert: true,
|
|
operation: 'white_label_backend_guard',
|
|
requestHostname: 'acount.accounted.se',
|
|
backendClassification: 'staging',
|
|
},
|
|
)
|
|
})
|
|
|
|
it('preserves canonical app behavior with the same backend', async () => {
|
|
const request = new NextRequest('https://app.accounted.se/login')
|
|
|
|
expect((await proxy(request)).status).toBe(204)
|
|
expect(updateSessionMock).toHaveBeenCalledOnce()
|
|
expect(updateSessionMock).toHaveBeenCalledWith(request)
|
|
})
|
|
|
|
it('does not treat the callback allowlist as a production classification', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_WHITELABEL_DOMAINS', 'internal-demo.accounted.test')
|
|
const request = new NextRequest('https://internal-demo.accounted.test/login')
|
|
|
|
expect((await proxy(request)).status).toBe(204)
|
|
expect(updateSessionMock).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('allows the production host after it moves to a different backend', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_SUPABASE_URL', PRODUCTION_URL)
|
|
const request = new NextRequest('https://acount.accounted.se/login')
|
|
|
|
expect((await proxy(request)).status).toBe(204)
|
|
expect(updateSessionMock).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('uses the request URL host and ignores x-forwarded-host', async () => {
|
|
const spoofedForwardedHost = new NextRequest(
|
|
'https://preview.vercel.app/login',
|
|
{ headers: { 'x-forwarded-host': 'acount.accounted.se' } },
|
|
)
|
|
expect((await proxy(spoofedForwardedHost)).status).toBe(204)
|
|
|
|
const productionHost = new NextRequest(
|
|
'https://acount.accounted.se/login',
|
|
{ headers: { 'x-forwarded-host': 'preview.vercel.app' } },
|
|
)
|
|
expect((await proxy(productionHost)).status).toBe(503)
|
|
expect(updateSessionMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|