Files
supabase/apps/studio/components/layouts/ProjectNeedsSecuring/ProjectNeedsSecuring.utils.test.ts
oniani1 29bfa7b75b fix(studio): encode special characters in project securing policies links (#45849)
Closes #45847.

## Summary

`ProjectNeedsSecuringView.tsx` built the `View policies` href on the
first-time security gate by interpolating `table.schema` and
`table.name` directly into the URL. A table or schema containing `&`,
`=`, `+`, or `#` corrupted the destination and routed the user to the
wrong policies filter on what is meant to be a guided onboarding flow.

Extracts the URL into `getTablePoliciesHref` in
`ProjectNeedsSecuring.utils.ts` with `encodeURIComponent` wraps, and
replaces the inline interpolation. Same pattern as #45385.

## Test plan

Added `ProjectNeedsSecuring.utils.test.ts` covering
`getTablePoliciesHref` (plain values, special chars in name, special
chars in schema, both, undefined inputs) and pulling in the
previously-untested `getTableKey`, `formatRlsDescription`, `sortTables`,
and `buildSecurityPromptMarkdown` utilities. Ten tests total.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Added comprehensive test coverage for security utilities, including
URL construction, formatting, sorting, and markdown report generation.

* **Refactor**
* Extracted URL building logic into a centralized utility function for
improved consistency and maintainability.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45849)

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-14 18:52:49 +00:00

97 lines
3.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { ProjectSecurityTable } from './ProjectNeedsSecuring.types'
import {
buildSecurityPromptMarkdown,
formatRlsDescription,
getTableKey,
getTablePoliciesHref,
sortTables,
} from './ProjectNeedsSecuring.utils'
const table = (overrides: Partial<ProjectSecurityTable>): ProjectSecurityTable => ({
id: 1,
name: 't',
schema: 'public',
rlsEnabled: false,
hasRlsIssue: false,
dataApiAccessible: false,
...overrides,
})
describe('ProjectNeedsSecuring.utils: getTableKey', () => {
it('joins schema and name with a dot', () => {
expect(getTableKey({ schema: 'public', name: 'users' })).toBe('public.users')
})
})
describe('ProjectNeedsSecuring.utils: formatRlsDescription', () => {
it('returns the singular form when count is 1', () => {
expect(formatRlsDescription(1)).toContain('1 table has RLS disabled')
expect(formatRlsDescription(1)).toContain('its data')
})
it('returns the plural form for any other count', () => {
expect(formatRlsDescription(3)).toContain('3 tables have RLS disabled')
expect(formatRlsDescription(3)).toContain('their data')
})
})
describe('ProjectNeedsSecuring.utils: sortTables', () => {
it('puts tables with active RLS issues first', () => {
const tables = [
table({ id: 1, name: 'a', hasRlsIssue: false, rlsEnabled: true }),
table({ id: 2, name: 'b', hasRlsIssue: true, rlsEnabled: false }),
table({ id: 3, name: 'c', hasRlsIssue: false, rlsEnabled: false }),
]
const sorted = sortTables(tables)
expect(sorted.map((t) => t.name)).toEqual(['b', 'c', 'a'])
})
})
describe('ProjectNeedsSecuring.utils: buildSecurityPromptMarkdown', () => {
it('builds a markdown report with a header row and one row per table', () => {
const markdown = buildSecurityPromptMarkdown(1, [
table({ name: 'invoices', schema: 'public', dataApiAccessible: true, rlsEnabled: false }),
])
expect(markdown).toContain('## Project security review')
expect(markdown).toContain('1 table has RLS disabled')
expect(markdown).toContain('| invoices | public | Yes | Disabled |')
})
})
describe('ProjectNeedsSecuring.utils: getTablePoliciesHref', () => {
it('builds the policies href with plain values', () => {
expect(getTablePoliciesHref('abc', 'public', 'invoices')).toBe(
'/project/abc/auth/policies?schema=public&search=invoices'
)
})
it('preserves special characters in the table name', () => {
const href = getTablePoliciesHref('abc', 'public', 'user_data&secret=1')
const parsed = new URL(href, 'http://example.com')
expect(parsed.searchParams.get('search')).toBe('user_data&secret=1')
expect(parsed.searchParams.get('schema')).toBe('public')
})
it('preserves special characters in the schema', () => {
const href = getTablePoliciesHref('abc', 'my schema+x', 'users')
const parsed = new URL(href, 'http://example.com')
expect(parsed.searchParams.get('schema')).toBe('my schema+x')
expect(parsed.searchParams.get('search')).toBe('users')
})
it('encodes both values together', () => {
const href = getTablePoliciesHref('abc', 'a&b=c', 'd e+f')
const parsed = new URL(href, 'http://example.com')
expect(parsed.searchParams.get('schema')).toBe('a&b=c')
expect(parsed.searchParams.get('search')).toBe('d e+f')
})
it('falls back to empty strings for undefined inputs', () => {
expect(getTablePoliciesHref(undefined, undefined, undefined)).toBe(
'/project//auth/policies?schema=&search='
)
})
})