Skip to content

Commit 35e1652

Browse files
authored
refactor: abstract platform dependencies for testability and add auth tests (#526)
# Add Testing Utilities for Edge Worker Platform Abstraction This PR adds a new `/testing` entry point to the edge-worker package that provides utilities for testing Supabase platform integrations. The key improvements include: 1. Created a platform dependency abstraction layer that decouples the SupabasePlatformAdapter from Deno globals 2. Added a `configurePlatform()` utility for overriding platform dependencies in tests 3. Implemented comprehensive unit tests for SupabasePlatformAdapter 4. Added E2E tests for authorization validation in production environments The implementation: - Extracts platform dependencies (env, serve, shutdown, etc.) into a dedicated interface - Provides a default implementation using Deno globals - Allows tests to override these dependencies without mocking globals - Includes a new auth_test edge function for E2E testing of authorization This approach makes the edge worker more testable while maintaining the same runtime behavior in production.
1 parent 17c5389 commit 35e1652

File tree

18 files changed

+855
-844
lines changed

18 files changed

+855
-844
lines changed

PLAN.md

Lines changed: 0 additions & 719 deletions
This file was deleted.

pkgs/edge-worker/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
"./_internal": {
1616
"types": "./dist/_internal.d.ts",
1717
"import": "./dist/_internal.js"
18+
},
19+
"./testing": {
20+
"types": "./dist/testing.d.ts",
21+
"import": "./dist/testing.js"
1822
}
1923
},
2024
"files": [

pkgs/edge-worker/scripts/sync-e2e-deps.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ cat > "$VENDOR_DIR/@pgflow/edge-worker/_internal.ts" << 'EOF'
5757
export * from './src/_internal.ts';
5858
EOF
5959

60+
# Create testing.ts redirect for test utilities
61+
cat > "$VENDOR_DIR/@pgflow/edge-worker/testing.ts" << 'EOF'
62+
// Re-export from the src directory to maintain compatibility
63+
export * from './src/testing.ts';
64+
EOF
65+
6066
# Verify key files exist
6167
if [ ! -f "$VENDOR_DIR/@pgflow/core/index.js" ]; then
6268
echo "⚠️ Warning: @pgflow/core/index.js not found after copy"

pkgs/edge-worker/src/platform/SupabasePlatformAdapter.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,7 @@ import {
1616
resolveSqlConnection,
1717
} from './resolveConnection.js';
1818
import { Queries } from '../core/Queries.js';
19-
20-
/**
21-
* Supabase Edge Runtime type (without global augmentation to comply with JSR)
22-
*/
23-
interface EdgeRuntimeType {
24-
waitUntil(promise: Promise<unknown>): void;
25-
}
19+
import { getPlatformDeps, type SupabasePlatformDeps } from './deps.js';
2620

2721
/**
2822
* Supabase-specific environment variables
@@ -54,13 +48,19 @@ export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResource
5448
private validatedEnv: SupabaseEnv;
5549
private _connectionString: string | undefined;
5650
private queries: Queries;
51+
private deps: SupabasePlatformDeps;
5752

5853
// Logging factory with dynamic workerId support
5954
private loggingFactory = createLoggingFactory();
6055

61-
constructor(options?: { sql?: Sql; connectionString?: string }) {
56+
constructor(
57+
options?: { sql?: Sql; connectionString?: string },
58+
deps: SupabasePlatformDeps = getPlatformDeps()
59+
) {
60+
this.deps = deps;
61+
6262
// Validate environment variables once at startup
63-
const env = Deno.env.toObject();
63+
const env = deps.getEnv();
6464
this.assertSupabaseEnv(env);
6565
this.validatedEnv = env;
6666

@@ -173,7 +173,7 @@ export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResource
173173
}
174174

175175
private setupShutdownHandler(): void {
176-
globalThis.onbeforeunload = async () => {
176+
this.deps.onShutdown(async () => {
177177
this.logger.debug('Shutting down...');
178178

179179
if (this.worker) {
@@ -184,7 +184,7 @@ export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResource
184184
}
185185

186186
await this.stopWorker();
187-
};
187+
});
188188
}
189189

190190
/**
@@ -195,15 +195,14 @@ export class SupabasePlatformAdapter implements PlatformAdapter<SupabaseResource
195195
* by passing a promise that never resolves.
196196
*/
197197
private extendLifetimeOfEdgeFunction(): void {
198-
// EdgeRuntime is available in Supabase Edge Functions runtime
199198
const promiseThatNeverResolves = new Promise(() => {
200199
// Intentionally empty - this promise never resolves to extend function lifetime
201200
});
202-
(globalThis as typeof globalThis & { EdgeRuntime: EdgeRuntimeType }).EdgeRuntime.waitUntil(promiseThatNeverResolves);
201+
this.deps.extendLifetime(promiseThatNeverResolves);
203202
}
204203

205204
private setupStartupHandler(createWorkerFn: CreateWorkerFn): void {
206-
Deno.serve({}, (req: Request) => {
205+
this.deps.serve((req: Request) => {
207206
// Validate auth header in production (skipped in local mode)
208207
const authResult = validateServiceRoleAuth(req, this.validatedEnv);
209208
if (!authResult.valid) {

pkgs/edge-worker/src/platform/createAdapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { PlatformAdapter } from './types.js';
22
import { SupabasePlatformAdapter } from './SupabasePlatformAdapter.js';
33
import type { SupabaseResources } from '@pgflow/dsl/supabase';
44
import type { Sql } from 'postgres';
5+
import { getPlatformDeps } from './deps.js';
56

67
interface AdapterOptions {
78
sql?: Sql;
@@ -15,7 +16,7 @@ export function createAdapter(options?: AdapterOptions): PlatformAdapter<Supabas
1516
if (isDenoEnvironment()) {
1617
// For now, always use SupabasePlatformAdapter for Deno
1718
// In the future, we could detect Supabase vs other Deno environments
18-
const adapter = new SupabasePlatformAdapter(options);
19+
const adapter = new SupabasePlatformAdapter(options, getPlatformDeps());
1920
return adapter;
2021
}
2122

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Platform dependencies interface for abstracting Deno/EdgeRuntime globals.
3+
* This enables unit testing of SupabasePlatformAdapter without mocking globals.
4+
*/
5+
export interface SupabasePlatformDeps {
6+
getEnv: () => Record<string, string | undefined>;
7+
onShutdown: (handler: () => void | Promise<void>) => void;
8+
extendLifetime: (promise: Promise<unknown>) => void;
9+
serve: (handler: (req: Request) => Response | Promise<Response>) => void;
10+
}
11+
12+
/**
13+
* Type for globalThis with EdgeRuntime available (Supabase Edge Functions).
14+
*/
15+
type EdgeRuntimeGlobal = typeof globalThis & {
16+
EdgeRuntime: { waitUntil(p: Promise<unknown>): void };
17+
};
18+
19+
/**
20+
* Default implementation using Deno runtime globals.
21+
*/
22+
const defaultDeps: SupabasePlatformDeps = {
23+
getEnv: () => Deno.env.toObject(),
24+
onShutdown: (h) => {
25+
globalThis.onbeforeunload = h;
26+
},
27+
extendLifetime: (p) => (globalThis as EdgeRuntimeGlobal).EdgeRuntime.waitUntil(p),
28+
serve: (h) => Deno.serve({}, h),
29+
};
30+
31+
let currentDeps = defaultDeps;
32+
33+
/**
34+
* Get current platform dependencies.
35+
* Used by createAdapter() to pass deps to SupabasePlatformAdapter.
36+
*/
37+
export const getPlatformDeps = (): SupabasePlatformDeps => currentDeps;
38+
39+
/**
40+
* Configure platform dependencies for testing.
41+
* Exported via /testing entry point.
42+
*
43+
* WARNING: This is a permanent override for the lifetime of the process.
44+
* There is no reset function - once configured, the overrides stay active.
45+
* This is intentional for edge functions where the mock must persist across
46+
* all HTTP requests. If scoped mocking with cleanup is needed in the future,
47+
* consider adding resetPlatform() and withMockPlatform() utilities.
48+
*/
49+
export function configurePlatform(overrides: Partial<SupabasePlatformDeps>): void {
50+
console.warn(
51+
'[pgflow] configurePlatform() called - platform deps permanently overridden:',
52+
Object.keys(overrides).join(', ')
53+
);
54+
currentDeps = { ...defaultDeps, ...overrides };
55+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './types.js';
22
export { createAdapter } from './createAdapter.js';
33
export { SupabasePlatformAdapter } from './SupabasePlatformAdapter.js';
4+
export type { SupabasePlatformDeps } from './deps.js';

pkgs/edge-worker/src/shared/localDetection.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,3 @@ export function isLocalSupabaseEnv(env: Record<string, string | undefined>): boo
1414
return anonKey === KNOWN_LOCAL_ANON_KEY ||
1515
serviceRoleKey === KNOWN_LOCAL_SERVICE_ROLE_KEY;
1616
}
17-
18-
export function isLocalSupabase(): boolean {
19-
return isLocalSupabaseEnv(Deno.env.toObject());
20-
}

pkgs/edge-worker/src/testing.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Test utilities for @pgflow/edge-worker
3+
*
4+
* Import via: import { ... } from '@pgflow/edge-worker/testing';
5+
*/
6+
7+
export {
8+
configurePlatform,
9+
type SupabasePlatformDeps,
10+
} from './platform/deps.js';

pkgs/edge-worker/supabase/config.toml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,45 @@ enabled = true
5656
verify_jwt = false
5757
import_map = "./functions/deno.json"
5858
entrypoint = "./functions/pgflow/index.ts"
59+
60+
[functions.auth_test]
61+
enabled = true
62+
verify_jwt = false
63+
import_map = "./functions/deno.json"
64+
entrypoint = "./functions/auth_test/index.ts"
65+
66+
[functions.conn_zero_config]
67+
enabled = true
68+
verify_jwt = false
69+
import_map = "./functions/deno.json"
70+
entrypoint = "./functions/conn_zero_config/index.ts"
71+
72+
[functions.conn_string]
73+
enabled = true
74+
verify_jwt = false
75+
import_map = "./functions/deno.json"
76+
entrypoint = "./functions/conn_string/index.ts"
77+
78+
[functions.conn_env_var]
79+
enabled = true
80+
verify_jwt = false
81+
import_map = "./functions/deno.json"
82+
entrypoint = "./functions/conn_env_var/index.ts"
83+
84+
[functions.conn_custom_sql]
85+
enabled = true
86+
verify_jwt = false
87+
import_map = "./functions/deno.json"
88+
entrypoint = "./functions/conn_custom_sql/index.ts"
89+
90+
[functions.stopped_at_test]
91+
enabled = true
92+
verify_jwt = false
93+
import_map = "./functions/deno.json"
94+
entrypoint = "./functions/stopped_at_test/index.ts"
95+
96+
[functions.retry-demo]
97+
enabled = true
98+
verify_jwt = false
99+
import_map = "./functions/deno.json"
100+
entrypoint = "./functions/retry-demo/index.ts"

0 commit comments

Comments
 (0)