ProjectNeedsSecuring.utils.test.ts96 lines · main
1import { describe, expect, it } from 'vitest'
2
3import type { ProjectSecurityTable } from './ProjectNeedsSecuring.types'
4import {
5 buildSecurityPromptMarkdown,
6 formatRlsDescription,
7 getTableKey,
8 getTablePoliciesHref,
9 sortTables,
10} from './ProjectNeedsSecuring.utils'
11
12const table = (overrides: Partial<ProjectSecurityTable>): ProjectSecurityTable => ({
13 id: 1,
14 name: 't',
15 schema: 'public',
16 rlsEnabled: false,
17 hasRlsIssue: false,
18 dataApiAccessible: false,
19 ...overrides,
20})
21
22describe('ProjectNeedsSecuring.utils: getTableKey', () => {
23 it('joins schema and name with a dot', () => {
24 expect(getTableKey({ schema: 'public', name: 'users' })).toBe('public.users')
25 })
26})
27
28describe('ProjectNeedsSecuring.utils: formatRlsDescription', () => {
29 it('returns the singular form when count is 1', () => {
30 expect(formatRlsDescription(1)).toContain('1 table has RLS disabled')
31 expect(formatRlsDescription(1)).toContain('its data')
32 })
33
34 it('returns the plural form for any other count', () => {
35 expect(formatRlsDescription(3)).toContain('3 tables have RLS disabled')
36 expect(formatRlsDescription(3)).toContain('their data')
37 })
38})
39
40describe('ProjectNeedsSecuring.utils: sortTables', () => {
41 it('puts tables with active RLS issues first', () => {
42 const tables = [
43 table({ id: 1, name: 'a', hasRlsIssue: false, rlsEnabled: true }),
44 table({ id: 2, name: 'b', hasRlsIssue: true, rlsEnabled: false }),
45 table({ id: 3, name: 'c', hasRlsIssue: false, rlsEnabled: false }),
46 ]
47 const sorted = sortTables(tables)
48 expect(sorted.map((t) => t.name)).toEqual(['b', 'c', 'a'])
49 })
50})
51
52describe('ProjectNeedsSecuring.utils: buildSecurityPromptMarkdown', () => {
53 it('builds a markdown report with a header row and one row per table', () => {
54 const markdown = buildSecurityPromptMarkdown(1, [
55 table({ name: 'invoices', schema: 'public', dataApiAccessible: true, rlsEnabled: false }),
56 ])
57 expect(markdown).toContain('## Project security review')
58 expect(markdown).toContain('1 table has RLS disabled')
59 expect(markdown).toContain('| invoices | public | Yes | Disabled |')
60 })
61})
62
63describe('ProjectNeedsSecuring.utils: getTablePoliciesHref', () => {
64 it('builds the policies href with plain values', () => {
65 expect(getTablePoliciesHref('abc', 'public', 'invoices')).toBe(
66 '/project/abc/auth/policies?schema=public&search=invoices'
67 )
68 })
69
70 it('preserves special characters in the table name', () => {
71 const href = getTablePoliciesHref('abc', 'public', 'user_data&secret=1')
72 const parsed = new URL(href, 'http://example.com')
73 expect(parsed.searchParams.get('search')).toBe('user_data&secret=1')
74 expect(parsed.searchParams.get('schema')).toBe('public')
75 })
76
77 it('preserves special characters in the schema', () => {
78 const href = getTablePoliciesHref('abc', 'my schema+x', 'users')
79 const parsed = new URL(href, 'http://example.com')
80 expect(parsed.searchParams.get('schema')).toBe('my schema+x')
81 expect(parsed.searchParams.get('search')).toBe('users')
82 })
83
84 it('encodes both values together', () => {
85 const href = getTablePoliciesHref('abc', 'a&b=c', 'd e+f')
86 const parsed = new URL(href, 'http://example.com')
87 expect(parsed.searchParams.get('schema')).toBe('a&b=c')
88 expect(parsed.searchParams.get('search')).toBe('d e+f')
89 })
90
91 it('falls back to empty strings for undefined inputs', () => {
92 expect(getTablePoliciesHref(undefined, undefined, undefined)).toBe(
93 '/project//auth/policies?schema=&search='
94 )
95 })
96})