Files
accounted/tests/tool-pg/client.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

106 lines
4.5 KiB
TypeScript

/**
* A REAL supabase-js client, pointed at a real PostgREST, over a real Postgres
* with every migration replayed.
*
* Why this is not the same thing as the pg-real suite: those tests hold a `pg`
* Pool and write SQL. The MCP tools do not write SQL. They call
* `supabase.from('x').select('a, b:c(d)')`, and the string inside `.select()`
* is parsed by PostgREST, not by Postgres. A misspelled column, a resource
* embed whose foreign key does not exist, an `or=(...)` whose grammar is
* slightly off, a `.contains()` against a non-jsonb column: every one of those
* is a runtime 400 from PostgREST that a mocked client answers cheerfully and a
* SQL test never reaches.
*/
import { createHmac } from 'node:crypto'
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
/** Must match PGRST_JWT_SECRET in tests/tool-pg/docker-compose.yml. */
export const TOOL_PG_JWT_SECRET =
'super-secret-jwt-token-with-at-least-32-characters-long'
export const TOOL_PG_REST_URL = process.env.TOOL_PG_REST_URL ?? 'http://127.0.0.1:54330'
export const TOOL_PG_DATABASE_URL =
process.env.TOOL_PG_DATABASE_URL ?? 'postgresql://postgres:postgres@127.0.0.1:54329/postgres'
function base64url(input: Buffer | string): string {
return Buffer.from(input)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
/**
* Hand-rolled HS256 rather than a JWT library: this repo is AGPL and audits its
* dependency surface, and a signed JWT is three base64url segments and one
* HMAC. Not worth a dependency, and definitely not worth one that ships only to
* tests.
*/
export function signServiceRoleJwt(secret = TOOL_PG_JWT_SECRET): string {
const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
const payload = base64url(
JSON.stringify({
role: 'service_role',
iss: 'tool-pg',
// Fixed far-future expiry: the harness is disposable and a clock-derived
// value would make an otherwise deterministic suite time-dependent.
exp: 4102444800,
}),
)
const signature = base64url(
createHmac('sha256', secret).update(`${header}.${payload}`).digest(),
)
return `${header}.${payload}.${signature}`
}
/**
* Service-role client, which is what the MCP server actually uses:
* `createServiceClientNoCookies()` on the API-key path. RLS is therefore NOT
* the thing under test here; the query grammar is. Tenant isolation on this
* surface comes from explicit `.eq('company_id', ...)` discipline, and a tool
* that forgets it is exactly the kind of bug these tests can catch.
*/
/**
* `createClient` eagerly constructs a RealtimeClient, which resolves a
* WebSocket implementation and throws "native WebSocket not found" on Node 20.
* CI runs Node 20; local machines may not, which is exactly the kind of
* difference that turns into a green local run and a red CI one.
*
* Nothing here subscribes to realtime, and RealtimeClient only RESOLVES the
* constructor rather than instantiating it, so handing it an inert class is
* enough. Bumping the job to Node 22 would work too, but it would make this the
* only job in the repo on a different runtime for a feature it never uses.
*/
class UnusedRealtimeTransport {
constructor() {
throw new Error('tool-pg: realtime is not used by these tests')
}
}
export function createToolPgClient(): SupabaseClient {
const key = signServiceRoleJwt()
return createClient(TOOL_PG_REST_URL, key, {
auth: { persistSession: false, autoRefreshToken: false },
db: { schema: 'public' },
realtime: { transport: UnusedRealtimeTransport as never },
global: {
headers: { apikey: key },
// supabase-js hard-codes a `/rest/v1` prefix onto every PostgREST
// request, because that is where Supabase's own gateway mounts it. A
// bare PostgREST serves at the root, so without this rewrite every
// query 404s.
//
// That is not a hypothetical: the first version of this harness omitted
// it, all 55 sweep queries 404d, the tools reported the empty response
// as "Database error: undefined", and the suite passed green while
// exercising nothing at all. The self-test in query-grammar.tool.test.ts
// exists to make that failure mode loud.
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
const rewritten = url.replace('/rest/v1/', '/').replace(/\/rest\/v1$/, '')
return fetch(rewritten, init)
},
},
})
}