supabase-js queries and auth tested with a mocked fetch (Vitest): working versions (tested 2026-10-15)
Use @supabase/supabase-js for table queries and auth in app code, and unit-test it offline by passing a fake fetch to createClient.
passed4 passed · 0 failed2026-10-15 11:57 UTC- Status
- passed
- Last passed
- 2026-10-15 11:57 UTC
- Tests
- 4 / 4
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("supabase-js-mocked-fetch") (install), or see them on GitHub.
{
"name": "supabase-js-mocked-fetch",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
},
"dependencies": {
"@supabase/supabase-js": "2.117.3"
},
"devDependencies": {
"@types/node": "24.19.2",
"vitest": "5.0.4"
}
}
import type { SupabaseClient } from "@supabase/supabase-js";
export type Todo = { id: number; title: string; done: boolean; user_id: string };
export async function openTodos(sb: SupabaseClient, userId: string): Promise<Todo[]> {
const { data, error } = await sb.from("todos").select("id,title,done,user_id").eq("user_id", userId).eq("done", false).order("id");
if (error) throw new Error(error.message);
return data as Todo[];
}
export async function addTodo(sb: SupabaseClient, userId: string, title: string): Promise<Todo> {
const { data, error } = await sb.from("todos").insert({ title, user_id: userId }).select().single();
if (error) throw new Error(error.message);
return data as Todo;
}
export async function signIn(sb: SupabaseClient, email: string, password: string) {
const { data, error } = await sb.auth.signInWithPassword({ email, password });
if (error) throw error;
return data.user?.id;
}
import { describe, it, expect, vi } from "vitest";
import { createClient } from "@supabase/supabase-js";
import { addTodo, openTodos, signIn } from "../src/todos";
const URL_ = "https://demo.supabase.co";
function client(handler: (url: URL, init: RequestInit) => Response) {
const fetch = vi.fn(async (input: RequestInfo | URL, init: RequestInit = {}) => handler(new URL(String(input instanceof Request ? input.url : input)), init));
const sb = createClient(URL_, "anon-key", { global: { fetch: fetch as any }, auth: { persistSession: false, autoRefreshToken: false } });
return { sb, fetch };
}
const ok = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
describe("todos", () => {
it("builds the PostgREST filter query", async () => {
const { sb, fetch } = client(() => ok([{ id: 1, title: "a", done: false, user_id: "u1" }]));
expect(await openTodos(sb, "u1")).toHaveLength(1);
const url = new URL(String(fetch.mock.calls[0][0]));
expect(url.pathname).toBe("/rest/v1/todos");
expect(url.searchParams.get("user_id")).toBe("eq.u1");
expect(url.searchParams.get("done")).toBe("eq.false");
});
it("insert + single returns the row", async () => {
const { sb, fetch } = client(() => ok({ id: 2, title: "b", done: false, user_id: "u1" }, 201));
expect((await addTodo(sb, "u1", "b")).id).toBe(2);
expect(fetch.mock.calls[0][1]?.method).toBe("POST");
});
it("surfaces PostgREST errors", async () => {
const { sb } = client(() => ok({ code: "42501", message: "permission denied for table todos", details: null, hint: null }, 403));
await expect(openTodos(sb, "u1")).rejects.toThrow(/permission denied/);
});
it("signs in through GoTrue", async () => {
const { sb, fetch } = client((url) => {
expect(url.pathname).toBe("/auth/v1/token");
expect(url.searchParams.get("grant_type")).toBe("password");
return ok({ access_token: "at", token_type: "bearer", expires_in: 3600, expires_at: Math.floor(Date.now() / 1000) + 3600, refresh_token: "rt", user: { id: "u1", aud: "authenticated", app_metadata: {}, user_metadata: {}, created_at: new Date().toISOString() } });
});
expect(await signIn(sb, "a@example.com", "pw")).toBe("u1");
expect(fetch).toHaveBeenCalledTimes(1);
});
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Astro 7 component rendering tests with the Container API (Vitest)
Render Astro components to HTML in Vitest with the Container API and getViteConfig, checking props, slots and conditional markup.
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().
Hono + Valibot request validation (Vitest)
Validate JSON bodies and query strings in Hono with Valibot schemas via @hono/valibot-validator, with custom error responses, tested with Vitest.
ioredis cache-aside with TTL, tested with ioredis-mock (Vitest)
Cache-aside helper on ioredis with JSON values, TTLs, stampede-safe single flight and invalidation, tested offline with ioredis-mock in Vitest.