Files
crewli/apps/app/src/observability/__tests__/scrubber.spec.ts
bert.hausmans 9247d89e4b test: scrubber + contextBinding regression coverage
WS-7 PR-3 commit 2.

- scrubber.spec.ts (18 cases): mirrors backend PiiScrubbingTest semantics.
  Body/header/query scrubbing, form_values wholesale replacement, all
  SENSITIVE_BODY_KEYS at top + nested levels, max_depth guard, cookies +
  storage + user.cookies sanitisation.
- contextBinding.spec.ts (11 cases): exercises the Vue Router beforeEach
  guard against a real router with mocked Sentry scope (capturing every
  setTag/setUser call into a per-test buffer). Cases:
    - portal-token zone — actor_scope=portal, no user_id
    - platform route + super_admin — actor_scope=platform
    - platform route without super_admin — does NOT tag platform
    - organizer route with active org — actor_scope=organisation +
      organisation_id
    - organizer route without active org — actor_scope=user, no org tag
    - unauthenticated public — actor_scope=anonymous
    - actor_type role hierarchy
    - RFC §3.8 ULID-only user identity (no email leakage)
    - route_name + app=app baseline tags
    - cross-zone leak guard: navigating from organizer to portal-token
      calls scope.clear() and does not bind user

Frontend test count 223 to 252. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:59:05 +02:00

207 lines
7.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { ErrorEvent as SentryErrorEvent } from '@sentry/vue'
import { scrubEvent } from '../scrubber'
function makeEvent(overrides: Partial<SentryErrorEvent> = {}): SentryErrorEvent {
return {
type: undefined,
...overrides,
} as SentryErrorEvent
}
describe('scrubEvent', () => {
describe('request body', () => {
it('scrubs password in request body', () => {
const event = makeEvent({ request: { data: { email: 'a@b.test', password: 'p@ss' } } })
const result = scrubEvent(event) as SentryErrorEvent
const data = result.request?.data as Record<string, unknown>
expect(data.password).toBe('[scrubbed]')
expect(data.email).toBe('a@b.test')
})
it('scrubs password_confirmation', () => {
const event = makeEvent({ request: { data: { password_confirmation: 'x', current_password: 'y' } } })
const result = scrubEvent(event) as SentryErrorEvent
const data = result.request?.data as Record<string, unknown>
expect(data.password_confirmation).toBe('[scrubbed]')
expect(data.current_password).toBe('[scrubbed]')
})
it('scrubs every SENSITIVE_BODY_KEY at top level', () => {
const allKeys = {
password: 'a',
token: 'b',
api_key: 'c',
secret: 'd',
webhook_secret: 'e',
dsn: 'f',
signature: 'g',
authorization: 'h',
cookie: 'i',
bearer: 'j',
iban: 'k',
bic: 'l',
passport_number: 'm',
bsn: 'n',
}
const event = makeEvent({ request: { data: allKeys } })
const result = scrubEvent(event) as SentryErrorEvent
const data = result.request?.data as Record<string, unknown>
for (const key of Object.keys(allKeys))
expect(data[key]).toBe('[scrubbed]')
})
it('scrubs sensitive keys at nested levels (recursive)', () => {
const event = makeEvent({
request: {
data: { profile: { address: { iban: 'NL91...', street: 'Damrak 1' } } },
},
})
const result = scrubEvent(event) as SentryErrorEvent
const data = result.request?.data as Record<string, Record<string, Record<string, unknown>>>
expect(data.profile.address.iban).toBe('[scrubbed]')
expect(data.profile.address.street).toBe('Damrak 1')
})
it('replaces form_values payload wholesale', () => {
const event = makeEvent({
request: { data: { form_values: { email: 'x@y.com', dietary: 'vegan' } } },
})
const result = scrubEvent(event) as SentryErrorEvent
const data = result.request?.data as Record<string, unknown>
expect(data.form_values).toBe('[scrubbed_form_values]')
const serialised = JSON.stringify(data)
expect(serialised).not.toContain('x@y.com')
expect(serialised).not.toContain('vegan')
})
it('does not leak email or other non-sensitive keys', () => {
const event = makeEvent({ request: { data: { email: 'x@y.com', name: 'Bob' } } })
const result = scrubEvent(event) as SentryErrorEvent
const data = result.request?.data as Record<string, unknown>
expect(data.email).toBe('x@y.com')
expect(data.name).toBe('Bob')
})
it('hits max_depth guard at depth 11', () => {
let deep: unknown = { v: 'leaf' }
for (let i = 0; i < 15; i++)
deep = { nest: deep }
const event = makeEvent({ request: { data: deep as Record<string, unknown> } })
const result = scrubEvent(event) as SentryErrorEvent
const serialised = JSON.stringify(result.request?.data)
expect(serialised).toContain('[max_depth]')
})
it('returns event unchanged when request is undefined', () => {
const event = makeEvent()
const result = scrubEvent(event)
expect(result).toBe(event)
expect(result?.request).toBeUndefined()
})
it('returns event unchanged when request.data is null', () => {
const event = makeEvent({ request: { data: null as unknown as undefined } })
const result = scrubEvent(event) as SentryErrorEvent
expect(result.request?.data).toBeNull()
})
})
describe('headers', () => {
it('scrubs Authorization header', () => {
const event = makeEvent({ request: { headers: { Authorization: 'Bearer abc' } } })
const result = scrubEvent(event) as SentryErrorEvent
expect((result.request?.headers as Record<string, string>).Authorization).toBe('[scrubbed]')
})
it('scrubs Cookie header', () => {
const event = makeEvent({ request: { headers: { Cookie: 'sess=abc' } } })
const result = scrubEvent(event) as SentryErrorEvent
expect((result.request?.headers as Record<string, string>).Cookie).toBe('[scrubbed]')
})
it('scrubs case-insensitive header names', () => {
const event = makeEvent({ request: { headers: { 'X-API-KEY': 'k', 'x-impersonation-token': 't' } } })
const result = scrubEvent(event) as SentryErrorEvent
const headers = result.request?.headers as Record<string, string>
expect(headers['X-API-KEY']).toBe('[scrubbed]')
expect(headers['x-impersonation-token']).toBe('[scrubbed]')
})
})
describe('query string', () => {
it('scrubs token query param', () => {
const event = makeEvent({ request: { query_string: 'token=abc&keep=me' } })
const result = scrubEvent(event) as SentryErrorEvent
const qs = result.request?.query_string as string
expect(qs).toContain('token=%5Bscrubbed%5D')
expect(qs).toContain('keep=me')
})
it('scrubs api_key query param', () => {
const event = makeEvent({ request: { query_string: 'api_key=xyz&page=2' } })
const result = scrubEvent(event) as SentryErrorEvent
const qs = result.request?.query_string as string
expect(qs).toContain('api_key=%5Bscrubbed%5D')
expect(qs).toContain('page=2')
})
it('preserves non-sensitive query params unchanged', () => {
const event = makeEvent({ request: { query_string: 'page=2&sort=name' } })
const result = scrubEvent(event) as SentryErrorEvent
const qs = result.request?.query_string as string
expect(qs).toBe('page=2&sort=name')
})
})
describe('cookies + storage', () => {
it('scrubs cookies wholesale', () => {
const event = makeEvent({ request: { cookies: { sess: 'abc', tracking: 'xyz' } } })
const result = scrubEvent(event) as SentryErrorEvent
expect(result.request?.cookies).toEqual({ scrubbed: '[scrubbed]' })
})
it('strips storage context if present (RFC §3.7 frontend point 2)', () => {
const event = makeEvent({
contexts: { storage: { local: { token: 'abc' } } } as unknown as SentryErrorEvent['contexts'],
})
const result = scrubEvent(event) as SentryErrorEvent
expect(result.contexts).not.toHaveProperty('storage')
})
it('strips user.cookies if present (RFC §3.7 frontend point 1)', () => {
const event = makeEvent({
user: { id: 'ulid', cookies: 'session=...' } as unknown as SentryErrorEvent['user'],
})
const result = scrubEvent(event) as SentryErrorEvent
expect(result.user).not.toHaveProperty('cookies')
expect(result.user?.id).toBe('ulid')
})
})
})