Files
supabase/apps/studio/components/interfaces/ConnectSheet/__tests__/ConnectionString.utils.test.ts
T
Joshen Lim 66bfc5fdc3 Refactor ConnectSheet + Add unit tests to cover various logic (#47764)
## Context

PR here mainly breaks up the files under `ConnectSheet` to separate the
functional logic so that we can write unit tests.
No behavior changes intended beyond the bug fixes

## Changes involved

- **Test organization:** moved all root-level `ConnectSheet` test files
into `ConnectSheet/__tests__/` for consistency with other parts of the
codebase that use this convention.
- **Bug fix:** read replica label had a stray `}` / missing `)`,
rendering as e.g. `Read Replica (us-east-1 - abc123})` instead of `Read
Replica (us-east-1 - abc123)`.
- **`ConnectSheet.tsx`:** extracted the "hydrate sheet state on open"
`useEffect` logic (mode/field/URL param resolution from URL vs.
localStorage) into a new `ConnectSheet.utils.ts`, with unit tests
- **`useConnectServerEnv.ts`:** fixed two race conditions in the secret
reveal/hide flow:
- `toggle()` and `getValue()` could each fire a separate reveal request
if triggered close together — now deduped to share one in-flight
request.
- `getValue()` could hide a secret that had just been explicitly
revealed by a concurrent `toggle()`, due to reading a stale closure
value — now reads the live state via `useLatest`.
- Also stopped swallowing the original error on reveal failure (now
attached via `cause`).
- Added tests for the above, plus the 10s auto-hide timer (previously
untested).
- **`ConnectStepsSection.tsx`:** extracted `resolveContentPath` and the
three inline "show notice" booleans (IPv4 addon, session pooler,
self-hosted MCP) into `ConnectStepsSection.utils.ts`, matching the
existing pattern for the Data API notice. Added unit tests for all of
them.

## To test
- [ ] Just a basic smoke test of the Connect sheet should do

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

* **New Features**
* Improved connect setup hydration so saved preferences and URL values
are applied more consistently when opening the sheet, including
automatic URL backfilling where needed.
* Refreshed connection guidance notices (IPv4 add-on, session pooler,
and self-hosted MCP) with more consistent logic.
* **Bug Fixes**
* Fixed secret reveal behavior to keep concurrent reveal actions in
sync, handle failures more safely, and ensure auto-hide works reliably.
  * Corrected the read-replica option label formatting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-10 15:24:34 +08:00

179 lines
5.6 KiB
TypeScript

import { describe, expect, test } from 'vitest'
import {
buildConnectionParameters,
buildConnectionStringWithPassword,
buildSafeConnectionString,
DEFAULT_PORT,
parseConnectionParams,
PASSWORD_PLACEHOLDER,
resolveConnectionString,
} from '../ConnectionString.utils'
describe('parseConnectionParams', () => {
test('returns hidden defaults for an empty string', () => {
expect(parseConnectionParams('')).toEqual({
host: 'hidden',
port: DEFAULT_PORT,
user: 'hidden',
database: 'hidden',
})
})
test('returns hidden defaults for an unparseable URL', () => {
expect(parseConnectionParams('not a url')).toEqual({
host: 'hidden',
port: DEFAULT_PORT,
user: 'hidden',
database: 'hidden',
})
})
test('parses a platform-shaped connection string', () => {
const uri =
'postgresql://postgres.projref:[YOUR-PASSWORD]@aws-0-eu-west-1.pooler.supabase.com:6543/postgres'
expect(parseConnectionParams(uri)).toEqual({
host: 'aws-0-eu-west-1.pooler.supabase.com',
port: '6543',
user: 'postgres.projref',
database: 'postgres',
})
})
test('decodes percent-encoded bracket placeholders in the user info', () => {
// The URL parser percent-encodes the `[`/`]` in self-hosted's POOLER_TENANT_ID placeholder.
// parseConnectionParams must decode so the displayed user matches what we wrote.
const uri =
'postgresql://postgres.[POOLER_TENANT_ID]:[YOUR-PASSWORD]@supabase.example.com:6543/postgres'
expect(parseConnectionParams(uri).user).toBe('postgres.[POOLER_TENANT_ID]')
})
})
describe('buildSafeConnectionString', () => {
test('returns empty string when input is empty', () => {
expect(buildSafeConnectionString('', parseConnectionParams(''))).toBe('')
})
test('rebuilds the URL with PASSWORD_PLACEHOLDER and the parsed params (round-trips brackets)', () => {
const uri =
'postgresql://postgres.[POOLER_TENANT_ID]:[YOUR-PASSWORD]@supabase.example.com:6543/postgres'
const params = parseConnectionParams(uri)
const safe = buildSafeConnectionString(uri, params)
expect(safe).toBe(
`postgresql://postgres.[POOLER_TENANT_ID]:${PASSWORD_PLACEHOLDER}@supabase.example.com:6543/postgres`
)
})
test('preserves search params from the original URL', () => {
const uri = 'postgresql://postgres.proj:[YOUR-PASSWORD]@host:5432/postgres?sslmode=require'
const params = parseConnectionParams(uri)
expect(buildSafeConnectionString(uri, params)).toContain('?sslmode=require')
})
})
describe('buildConnectionStringWithPassword', () => {
test('returns the original string when input or password is empty', () => {
const uri = `postgresql://postgres:${PASSWORD_PLACEHOLDER}@localhost:5432/postgres`
expect(buildConnectionStringWithPassword('', 'password')).toBe('')
expect(buildConnectionStringWithPassword(uri, '')).toBe(uri)
})
test('replaces every password placeholder with the encoded password', () => {
const uri = `postgresql://postgres:${PASSWORD_PLACEHOLDER}@localhost:5432/postgres?password=${PASSWORD_PLACEHOLDER}`
expect(buildConnectionStringWithPassword(uri, 'p@ss/word#1')).toBe(
'postgresql://postgres:p%40ss%2Fword%231@localhost:5432/postgres?password=p%40ss%2Fword%231'
)
})
})
describe('resolveConnectionString', () => {
const pooler = {
transactionShared: 'tx-shared',
sessionShared: 'session-shared',
transactionDedicated: 'tx-dedicated',
sessionDedicated: 'session-dedicated',
ipv4SupportedForDedicatedPooler: true,
direct: 'direct-uri',
}
test('returns empty string when pooler bag is undefined', () => {
expect(
resolveConnectionString({
connectionMethod: 'direct',
useSharedPooler: false,
connectionStringPooler: undefined,
})
).toBe('')
})
test('direct method returns the direct URI', () => {
expect(
resolveConnectionString({
connectionMethod: 'direct',
useSharedPooler: false,
connectionStringPooler: pooler,
})
).toBe('direct-uri')
})
test('session method returns sessionShared', () => {
expect(
resolveConnectionString({
connectionMethod: 'session',
useSharedPooler: false,
connectionStringPooler: pooler,
})
).toBe('session-shared')
})
test('transaction prefers dedicated when available and useSharedPooler is false', () => {
expect(
resolveConnectionString({
connectionMethod: 'transaction',
useSharedPooler: false,
connectionStringPooler: pooler,
})
).toBe('tx-dedicated')
})
test('transaction falls back to shared when useSharedPooler is true', () => {
expect(
resolveConnectionString({
connectionMethod: 'transaction',
useSharedPooler: true,
connectionStringPooler: pooler,
})
).toBe('tx-shared')
})
test('transaction falls back to shared when no dedicated pooler exists', () => {
expect(
resolveConnectionString({
connectionMethod: 'transaction',
useSharedPooler: false,
connectionStringPooler: { ...pooler, transactionDedicated: undefined },
})
).toBe('tx-shared')
})
})
describe('buildConnectionParameters', () => {
test('produces host/port/database/user rows in display order', () => {
expect(
buildConnectionParameters({
host: 'h',
port: '5432',
user: 'u',
database: 'd',
})
).toEqual([
{ key: 'host', value: 'h' },
{ key: 'port', value: '5432' },
{ key: 'database', value: 'd' },
{ key: 'user', value: 'u' },
])
})
})