| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- import { describe, expect, it } from 'vitest'
- import {
- getFacetCountQuery,
- getLogsChartQuery,
- getLogsCountQuery,
- getUnifiedLogsQuery,
- } from './UnifiedLogs.queries'
- import { getUnifiedLogsQuery as getUnifiedLogsQueryBQ } from './UnifiedLogs.queries.bq'
- const baseSearch = {
- date: [new Date('2026-05-08T09:00:00Z'), new Date('2026-05-08T10:00:00Z')],
- } as any
- describe('UnifiedLogs.queries (OTEL flat)', () => {
- describe('getUnifiedLogsQuery', () => {
- it('defaults to postgres + postgrest log types when none specified', () => {
- const sql = getUnifiedLogsQuery(baseSearch)
- expect(sql).toContain(`source = 'postgres_logs'`)
- // postgrest = edge_logs filtered by /rest/ path
- expect(sql).toContain(
- `source = 'edge_logs' AND log_attributes['request.path'] LIKE '%/rest/%'`
- )
- })
- it('routes the `edge` log type to edge_logs without /rest/ or /storage/ paths', () => {
- const sql = getUnifiedLogsQuery({ ...baseSearch, log_type: ['edge'] } as any)
- expect(sql).toContain(`NOT LIKE '%/rest/%'`)
- expect(sql).toContain(`NOT LIKE '%/storage/%'`)
- const where = sql.split(/\bWHERE\b/)[1] ?? ''
- expect(where).not.toContain(`source = 'postgres_logs'`)
- })
- it('routes the `storage` log type to edge_logs filtered by /storage/', () => {
- const sql = getUnifiedLogsQuery({ ...baseSearch, log_type: ['storage'] } as any)
- expect(sql).toContain(
- `source = 'edge_logs' AND log_attributes['request.path'] LIKE '%/storage/%'`
- )
- })
- it('escapes single quotes in filter values to prevent SQL injection', () => {
- const sql = getUnifiedLogsQuery({
- ...baseSearch,
- method: [`G'ET`],
- pathname: `/customers'; DROP TABLE logs --`,
- } as any)
- // Single quotes are doubled (SQL-standard escaping) by pg-meta's
- // literal(); a raw single quote from user input never closes its
- // string literal early.
- expect(sql).toContain(`'G''ET'`)
- expect(sql).toContain(`%/customers''; DROP TABLE logs --%`)
- })
- it('translates method/status/pathname filters to log_attributes predicates', () => {
- const sql = getUnifiedLogsQuery({
- ...baseSearch,
- method: ['GET'],
- status: ['401'],
- pathname: '/customers',
- } as any)
- expect(sql).toContain(`log_attributes['request.method'] IN ('GET')`)
- // Status filter wraps the CASE that picks HTTP code or Postgres SQLSTATE
- // so e.g. '00000' matches postgres success rows.
- expect(sql).toContain(`log_attributes['parsed.sql_state_code']`)
- expect(sql).toMatch(/END\) IN \('401'\)/)
- expect(sql).toContain(`log_attributes['request.path'] LIKE '%/customers%'`)
- })
- it('does not emit subqueries or CTEs (rejected by the OTEL endpoint)', () => {
- const sql = getUnifiedLogsQuery(baseSearch)
- expect(sql).not.toMatch(/WITH\s+\w+\s+AS\s*\(/i)
- expect(sql).not.toMatch(/FROM\s*\(\s*SELECT/i)
- // Single SELECT * FROM logs (not "SELECT *" wildcard usage either).
- expect(sql).not.toMatch(/SELECT\s+\*/)
- })
- })
- describe('getLogsCountQuery', () => {
- it('emits one UNION ALL branch per log_type bucket and per level', () => {
- const sql = getLogsCountQuery(baseSearch)
- // Per-log-type counts
- for (const lt of ['edge', 'postgrest', 'storage', 'postgres', 'edge function', 'auth']) {
- expect(sql).toContain(`'${lt}'`)
- }
- // Per-level counts
- for (const lvl of ['success', 'warning', 'error']) {
- expect(sql).toContain(`'${lvl}'`)
- }
- // Bundled via UNION ALL — multiple occurrences expected
- expect(sql.match(/UNION ALL/g)?.length ?? 0).toBeGreaterThan(5)
- })
- it('honours an active log_type filter in the total count branch', () => {
- const sql = getLogsCountQuery({ ...baseSearch, log_type: ['edge'] } as any)
- // The first branch is the total — its WHERE must include the edge
- // log_type predicate, otherwise the total badge would over-count
- // when a log_type filter is active.
- const totalBranch = sql.split(/\bUNION ALL\b/)[0]
- expect(totalBranch).toContain(`'total'`)
- expect(totalBranch).toContain(`source = 'edge_logs'`)
- expect(totalBranch).not.toContain(`source = 'postgres_logs'`)
- })
- })
- describe('getLogsChartQuery', () => {
- it('uses minute bucketing for short ranges', () => {
- const sql = getLogsChartQuery(baseSearch)
- expect(sql).toContain('toStartOfMinute(timestamp)')
- })
- it('uses hour bucketing for ranges spanning more than 12 hours', () => {
- const sql = getLogsChartQuery({
- ...baseSearch,
- date: [new Date('2026-05-08T00:00:00Z'), new Date('2026-05-08T18:00:00Z')],
- } as any)
- expect(sql).toContain('toStartOfHour(timestamp)')
- })
- it('uses day bucketing for ranges spanning more than 2 days', () => {
- const sql = getLogsChartQuery({
- ...baseSearch,
- date: [new Date('2026-05-01T00:00:00Z'), new Date('2026-05-08T00:00:00Z')],
- } as any)
- expect(sql).toContain('toStartOfDay(timestamp)')
- })
- })
- describe('getFacetCountQuery', () => {
- it('groups by the requested facet and excludes that facet from the WHERE filters', () => {
- const sql = getFacetCountQuery({
- search: { ...baseSearch, method: ['GET'], status: ['200'] } as any,
- facet: 'method',
- })
- // Filtered facet is excluded from WHERE; other filters still applied.
- // For facet='method' the SELECT projection doesn't include STATUS_EXPR,
- // so SQLSTATE/IN ('200') must come from the WHERE clause.
- expect(sql).not.toContain(`log_attributes['request.method'] IN ('GET')`)
- expect(sql).toContain(`log_attributes['parsed.sql_state_code']`)
- expect(sql).toMatch(/END\) IN \('200'\)/)
- expect(sql).toContain('GROUP BY value')
- expect(sql).toContain('LIMIT 20')
- })
- })
- describe('analyticsLiteral escaping', () => {
- it('emits ClickHouse / BigQuery escape syntax (doubled `\\\\`, no `E` prefix)', () => {
- // pg-meta's literal() would emit `E'a\\b'` for `a\b` — the `E` prefix is
- // Postgres-only and rejected by both analytics engines. analyticsLiteral
- // doubles the backslash inside plain `'…'` delimiters instead.
- const sql = getUnifiedLogsQuery({ ...baseSearch, method: 'a\\b' } as any)
- expect(sql).toContain(`log_attributes['request.method'] = 'a\\\\b'`)
- expect(sql).not.toContain(`E'a`)
- })
- it("escapes single quotes by doubling them ('' rather than \\')", () => {
- const sql = getUnifiedLogsQuery({ ...baseSearch, method: "GET' OR '1'='1" } as any)
- expect(sql).toContain(`log_attributes['request.method'] = 'GET'' OR ''1''=''1'`)
- })
- })
- })
- describe('UnifiedLogs.queries.bq', () => {
- it('backtick-quotes column identifiers (BigQuery syntax)', () => {
- const sql = getUnifiedLogsQueryBQ({ ...baseSearch, method: 'GET' } as any)
- expect(sql).toContain("`method` = 'GET'")
- })
- it('rejects keys with non-identifier characters', () => {
- // A key like "foo; DROP TABLE" fails the bqIdent regex (the `;` is not
- // in `[A-Za-z_][A-Za-z0-9_]*`), so the predicate is dropped entirely.
- const sql = getUnifiedLogsQueryBQ({ ...baseSearch, 'foo; DROP TABLE x': 'y' } as any)
- expect(sql).not.toContain('DROP TABLE')
- expect(sql).not.toContain('foo;')
- })
- it('rejects keys containing spaces', () => {
- const sql = getUnifiedLogsQueryBQ({
- ...baseSearch,
- 'level OR id IS NOT NULL': 'anything',
- } as any)
- expect(sql).not.toContain('IS NOT NULL')
- expect(sql).not.toContain('level OR')
- })
- it('escapes injection attempts in filter values via analyticsLiteral', () => {
- const sql = getUnifiedLogsQueryBQ({ ...baseSearch, method: "GET' OR '1'='1" } as any)
- // The value is single-quote-escaped, so the synthetic OR can't break out
- // of the string literal.
- expect(sql).toContain("`method` = 'GET'' OR ''1''=''1'")
- expect(sql).not.toMatch(/`method` = 'GET' OR '1'='1'/)
- })
- })
|