Files
supabase/apps/studio/components/interfaces/Linter/Linter.utils.test.tsx
oniani1 a95b6f9013 fix(studio): encode special characters in database advisor lint links (#45385)
The link builders in
`apps/studio/components/interfaces/Linter/Linter.utils.tsx` interpolate
`metadata.schema` and `metadata.name` directly into URL query strings. A
schema or table name with `&`, `=`, `+`, or `#` breaks the destination
filter on the linked page because `URLSearchParams` stops at the bare
`&` and decodes `+` to a space.

The `public_bucket_allows_listing` lint at line 338 already wraps
`bucket_id` in `encodeURIComponent`. The other 15 builders did not. This
wraps each `metadata?.schema` and `metadata?.name` interpolation with
`encodeURIComponent(value ?? '')` to match.

Added `Linter.utils.test.tsx` that constructs links with a schema
`a&b=c` and a name `d e+f` and asserts `URLSearchParams` round-trips
them. The bucket precedent is also covered.

Closes #45384

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved URL encoding for navigation links in the linter interface to
ensure proper handling of special characters in database, schema, and
table names.

* **Tests**
* Added test coverage for URL generation functionality in the linter
utility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 22:02:07 +08:00

69 lines
2.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { lintInfoMap } from './Linter.utils'
import { Lint } from '@/data/lint/lint-query'
const projectRef = 'abc'
const trickySchema = 'a&b=c'
const trickyName = 'd e+f'
describe('Linter.utils lintInfoMap link encoding', () => {
const cases: Array<{ name: string; nameParam?: string; hasSchema: boolean }> = [
{ name: 'unindexed_foreign_keys', hasSchema: true },
{ name: 'unused_index', nameParam: 'table', hasSchema: true },
{ name: 'multiple_permissive_policies', nameParam: 'search', hasSchema: true },
{ name: 'policy_exists_rls_disabled', nameParam: 'search', hasSchema: true },
{ name: 'rls_enabled_no_policy', nameParam: 'search', hasSchema: true },
{ name: 'duplicate_index', nameParam: 'table', hasSchema: true },
{ name: 'function_search_path_mutable', nameParam: 'search', hasSchema: true },
{ name: 'rls_disabled_in_public', nameParam: 'search', hasSchema: true },
{ name: 'extension_in_public', nameParam: 'filter', hasSchema: false },
{ name: 'sensitive_columns_exposed', nameParam: 'table', hasSchema: true },
{ name: 'rls_policy_always_true', nameParam: 'search', hasSchema: true },
{ name: 'pg_graphql_anon_table_exposed', nameParam: 'table', hasSchema: true },
{ name: 'pg_graphql_authenticated_table_exposed', nameParam: 'table', hasSchema: true },
{ name: 'anon_security_definer_function_executable', nameParam: 'search', hasSchema: true },
{
name: 'authenticated_security_definer_function_executable',
nameParam: 'search',
hasSchema: true,
},
]
for (const { name, nameParam, hasSchema } of cases) {
it(`preserves special characters in metadata for ${name}`, () => {
const info = lintInfoMap.find((entry) => entry.name === name)
expect(info, `expected ${name} in lintInfoMap`).toBeDefined()
const url = info!.link({
projectRef,
metadata: {
schema: trickySchema,
name: trickyName,
} as unknown as Lint['metadata'],
})
const parsed = new URL(url, 'http://example.com')
if (hasSchema) {
expect(parsed.searchParams.get('schema')).toBe(trickySchema)
}
if (nameParam) {
expect(parsed.searchParams.get(nameParam)).toBe(trickyName)
}
})
}
it('preserves slash and space in bucket_id for public_bucket_allows_listing', () => {
const info = lintInfoMap.find((entry) => entry.name === 'public_bucket_allows_listing')
expect(info).toBeDefined()
const url = info!.link({
projectRef,
metadata: {
bucket_id: 'a/b c',
} as unknown as Lint['metadata'],
})
expect(url).toBe('/project/abc/storage/files/buckets/a%2Fb%20c')
})
})