Skip to main content سوق المهارات اكتشف واستكشف مهارات الذكاء الاصطناعي التي بناها المجتمع.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
نسخ Promptعرض تفاصيل Prompt يتجاوز الأمر المباشر Prompt المخصّص للمراجعة. افحص المصدر قبل تشغيله.
npx skills add https://github.com/PramodDutta/qaskills --skill supabase-testingيبقى الأمر في سطر واحد. مرّر أفقيًا لمراجعته كاملًا قبل النسخ.
تفضّل نسخة محلية؟ نزّل الملفات المتاحة حاليًا لدى SkillsMP.
تحميل Zip جاري التحميل... المزيد من هذا المستودع Use when publishing SEO blog articles to qaskills.sh, e.g. "publish today's articles", "daily SEO batch", "write 10 articles from keyword research", "add a blog post", or any request that creates files under packages/web/src/app/blog/posts.
secure-test-data-engineer Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.
المهن ذات الصلة SOC
استنادا إلى تصنيف SOC المهني
name Supabase Testing description Testing patterns for Supabase applications covering auth flow testing, Row Level Security policy testing, realtime subscription testing, and edge function testing version 1.0.0 author thetestingacademy license MIT tags ["supabase","rls","auth","realtime","edge-functions","postgres","storage","migrations","row-level-security","testing"] testingTypes ["unit","integration","e2e"] frameworks ["vitest","jest","playwright"] languages ["typescript","javascript"] domains ["web","api","backend"] agents ["claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","gemini-cli","amp"]
Supabase Testing Skill
You are an expert QA engineer specializing in testing Supabase-powered applications. When the user asks you to write, review, or debug tests for Supabase auth, Row Level Security, realtime subscriptions, edge functions, or storage, follow these detailed instructions.
Core Principles
Local-first testing -- Always use Supabase CLI (supabase start) to run a local Postgres instance with all Supabase features. Never test directly against production.
RLS is your security boundary -- Row Level Security policies are the most critical code in a Supabase app. Test every policy with multiple user roles and edge cases.
Test auth flows end-to-end -- Supabase auth involves email confirmation, magic links, OAuth redirects, and JWT tokens. Test the full flow, not just the API call.
Realtime requires connection testing -- Subscription tests must establish actual WebSocket connections. Mock-only tests miss critical reconnection and conflict scenarios.
Edge functions are isolated -- Supabase Edge Functions run in Deno. Test them independently with their own test runner before integration testing.
Storage policies mirror RLS -- Storage bucket policies follow the same RLS pattern. Apply the same testing rigor to file access as to row access.
Migration testing prevents data loss -- Every schema migration should be tested against a snapshot of production-like data to verify both the up and down paths.
Project Structure
Always organize Supabase testing with this structure:
supabase/
config.toml
migrations/
20240101000000_create_profiles.sql
20240102000000_add_rls_policies.sql
functions/
hello-world/
index.ts
send-notification/
index.ts
seed.sql
tests/
rls/
profiles.test.ts
posts.test.ts
comments.test.ts
migrations/
migration.test.ts
src/
lib/
supabase/
client.ts
server.ts
middleware.ts
__tests__/
unit/
auth.test.ts
database.test.ts
integration/
auth-flow.test.ts
realtime.test.ts
storage.test.ts
e2e/
signup-login.spec.ts
crud-with-rls.spec.ts
helpers/
supabase-test-utils.ts
test-users.ts
seed-test-data.ts
Supabase Local Setup for Testing
Configuration
[project]
id = "my-project"
[db]
=
=
=
=
=
=
port
54322
[auth]
site_url
"http://localhost:3000"
enable_signup
true
[auth.email]
enable_confirmations
false
double_confirm_changes
false
[auth.external.google]
enabled
false
Test Helper Setup
import { createClient, SupabaseClient } from '@supabase/supabase-js' ;
const SUPABASE_URL = process.env .SUPABASE_URL || 'http://127.0.0.1:54321' ;
const SUPABASE_ANON_KEY = process.env .SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' ;
const SUPABASE_SERVICE_KEY = process.env .SUPABASE_SERVICE_ROLE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' ;
export function createAnonClient ( ): SupabaseClient {
return createClient (SUPABASE_URL , SUPABASE_ANON_KEY );
}
export function createServiceClient ( ): SupabaseClient {
return createClient (SUPABASE_URL , SUPABASE_SERVICE_KEY );
}
export async function createAuthenticatedClient (
email : string ,
password : string
): Promise <SupabaseClient > {
const client = createAnonClient ();
const { error } = await client.auth .signInWithPassword ({ email, password });
if (error) throw new Error (`Auth failed for ${email} : ${error.message} ` );
return client;
}
export async function createTestUser (
email ?: string ,
password ?: string ,
metadata ?: Record <string , unknown >
): Promise <{ client : SupabaseClient ; userId : string ; email : string }> {
const testEmail = email || `test-${Date .now()} -${Math .random().toString(36 ).slice(2 )} @test.com` ;
const testPassword = password || 'TestPassword123!' ;
const serviceClient = createServiceClient ();
const { data : user, error } = await serviceClient.auth .admin .createUser ({
email : testEmail,
password : testPassword,
email_confirm : true ,
user_metadata : metadata || {},
});
if (error) throw new Error (`Failed to create test user: ${error.message} ` );
const client = await createAuthenticatedClient (testEmail, testPassword);
return {
client,
userId : user.user .id ,
email : testEmail,
};
}
export async function cleanupTestUser (userId : string ): Promise <void > {
const serviceClient = createServiceClient ();
await serviceClient.auth .admin .deleteUser (userId);
}
export async function resetDatabase ( ): Promise <void > {
const serviceClient = createServiceClient ();
await serviceClient.from ('comments' ).delete ().neq ('id' , '00000000-0000-0000-0000-000000000000' );
await serviceClient.from ('posts' ).delete ().neq ('id' , '00000000-0000-0000-0000-000000000000' );
await serviceClient.from ('profiles' ).delete ().neq ('id' , '00000000-0000-0000-0000-000000000000' );
}
import { createTestUser, cleanupTestUser } from './supabase-test-utils' ;
import type { SupabaseClient } from '@supabase/supabase-js' ;
interface TestUserContext {
client : SupabaseClient ;
userId : string ;
email : string ;
}
export class TestUserManager {
private users : TestUserContext [] = [];
async createUser (
role : string = 'user' ,
metadata : Record <string , unknown > = {}
): Promise <TestUserContext > {
const user = await createTestUser (undefined , undefined , { role, ...metadata });
this .users .push (user);
return user;
}
async createAdmin (): Promise <TestUserContext > {
return this .createUser ('admin' );
}
async createModerator (): Promise <TestUserContext > {
return this .createUser ('moderator' );
}
async cleanup (): Promise <void > {
for (const user of this .users ) {
await cleanupTestUser (user.userId ).catch (() => {});
}
this .users = [];
}
}
Row Level Security (RLS) Testing
Schema and Policies
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY ,
username TEXT UNIQUE NOT NULL ,
bio TEXT,
avatar_url TEXT,
is_public BOOLEAN DEFAULT true ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT
USING (is_public = true );
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
CREATE POLICY "Users can insert own profile"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
CREATE POLICY "Admins can delete profiles"
ON profiles FOR DELETE
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid()
AND (auth.jwt() - > 'user_metadata' - >> 'role' ) = 'admin'
)
);
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY ,
author_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL ,
title TEXT NOT NULL ,
content TEXT NOT NULL ,
published BOOLEAN DEFAULT false ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Published posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true );
CREATE POLICY "Authors can view own drafts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Authors can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Authors can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
RLS Policy Tests
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest' ;
import {
createAnonClient,
createServiceClient,
resetDatabase,
} from '../../helpers/supabase-test-utils' ;
import { TestUserManager } from '../../helpers/test-users' ;
describe ('Profiles RLS Policies' , () => {
const users = new TestUserManager ();
let serviceClient : ReturnType <typeof createServiceClient>;
beforeAll (async () => {
serviceClient = createServiceClient ();
});
afterAll (async () => {
await users.cleanup ();
});
beforeEach (async () => {
await resetDatabase ();
});
describe ('SELECT policies' , () => {
it ('should allow anyone to view public profiles' , async () => {
const { userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : 'publicuser' ,
bio : 'Public bio' ,
is_public : true ,
});
const anonClient = createAnonClient ();
const { data, error } = await anonClient
.from ('profiles' )
.select ('*' )
.eq ('id' , userId)
.single ();
expect (error).toBeNull ();
expect (data).toBeDefined ();
expect (data!.username ).toBe ('publicuser' );
});
it ('should hide private profiles from anonymous users' , async () => {
const { userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : 'privateuser' ,
bio : 'Private bio' ,
is_public : false ,
});
const anonClient = createAnonClient ();
const { data } = await anonClient
.from ('profiles' )
.select ('*' )
.eq ('id' , userId);
expect (data).toEqual ([]);
});
it ('should allow users to view their own private profile' , async () => {
const { client, userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : 'myprofile' ,
bio : 'My private bio' ,
is_public : false ,
});
const { data, error } = await client
.from ('profiles' )
.select ('*' )
.eq ('id' , userId)
.single ();
expect (error).toBeNull ();
expect (data!.username ).toBe ('myprofile' );
});
it ('should hide private profiles from other authenticated users' , async () => {
const { userId : ownerId } = await users.createUser ();
const { client : otherClient } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : ownerId,
username : 'secretuser' ,
is_public : false ,
});
const { data } = await otherClient
.from ('profiles' )
.select ('*' )
.eq ('id' , ownerId);
expect (data).toEqual ([]);
});
});
describe ('INSERT policies' , () => {
it ('should allow users to create their own profile' , async () => {
const { client, userId } = await users.createUser ();
const { error } = await client.from ('profiles' ).insert ({
id : userId,
username : `user-${Date .now()} ` ,
bio : 'My bio' ,
});
expect (error).toBeNull ();
});
it ('should prevent users from creating profiles for other users' , async () => {
const { client } = await users.createUser ();
const { userId : otherUserId } = await users.createUser ();
const { error } = await client.from ('profiles' ).insert ({
id : otherUserId,
username : 'impersonator' ,
});
expect (error).not .toBeNull ();
expect (error!.code ).toBe ('42501' );
});
it ('should prevent anonymous users from creating profiles' , async () => {
const anonClient = createAnonClient ();
const { error } = await anonClient.from ('profiles' ).insert ({
id : '550e8400-e29b-41d4-a716-446655440000' ,
username : 'anonymous' ,
});
expect (error).not .toBeNull ();
});
});
describe ('UPDATE policies' , () => {
it ('should allow users to update their own profile' , async () => {
const { client, userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : `user-${Date .now()} ` ,
bio : 'Original bio' ,
});
const { error } = await client
.from ('profiles' )
.update ({ bio : 'Updated bio' })
.eq ('id' , userId);
expect (error).toBeNull ();
const { data } = await client
.from ('profiles' )
.select ('bio' )
.eq ('id' , userId)
.single ();
expect (data!.bio ).toBe ('Updated bio' );
});
it ('should prevent users from updating other profiles' , async () => {
const { userId : ownerId } = await users.createUser ();
const { client : attackerClient } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : ownerId,
username : `target-${Date .now()} ` ,
bio : 'Original' ,
});
const { data } = await attackerClient
.from ('profiles' )
.update ({ bio : 'Hacked!' })
.eq ('id' , ownerId)
.select ();
expect (data).toEqual ([]);
const { data : original } = await serviceClient
.from ('profiles' )
.select ('bio' )
.eq ('id' , ownerId)
.single ();
expect (original!.bio ).toBe ('Original' );
});
});
describe ('DELETE policies' , () => {
it ('should allow admins to delete profiles' , async () => {
const { client : adminClient } = await users.createAdmin ();
const { userId : targetId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : targetId,
username : `delete-target-${Date .now()} ` ,
});
const adminUserId = (await adminClient.auth .getUser ()).data .user !.id ;
await serviceClient.from ('profiles' ).insert ({
id : adminUserId,
username : `admin-${Date .now()} ` ,
});
const { error } = await adminClient
.from ('profiles' )
.delete ()
.eq ('id' , targetId);
expect (error).toBeNull ();
});
it ('should prevent non-admin users from deleting profiles' , async () => {
const { client : regularClient } = await users.createUser ();
const { userId : targetId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : targetId,
username : `no-delete-${Date .now()} ` ,
});
const { data } = await regularClient
.from ('profiles' )
.delete ()
.eq ('id' , targetId)
.select ();
expect (data).toEqual ([]);
});
});
});
Posts RLS Tests
import { describe, it, expect, beforeAll, afterAll } from 'vitest' ;
import {
createAnonClient,
createServiceClient,
} from '../../helpers/supabase-test-utils' ;
import { TestUserManager } from '../../helpers/test-users' ;
describe ('Posts RLS Policies' , () => {
const users = new TestUserManager ();
let serviceClient : ReturnType <typeof createServiceClient>;
beforeAll (() => {
serviceClient = createServiceClient ();
});
afterAll (async () => {
await users.cleanup ();
});
it ('should allow anyone to see published posts' , async () => {
const { userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : `author-${Date .now()} ` ,
});
await serviceClient.from ('posts' ).insert ({
author_id : userId,
title : 'Published Post' ,
content : 'This is public' ,
published : true ,
});
const anonClient = createAnonClient ();
const { data } = await anonClient
.from ('posts' )
.select ('*' )
.eq ('published' , true );
expect (data!.length ).toBeGreaterThan (0 );
expect (data!.some ((p ) => p.title === 'Published Post' )).toBe (true );
});
it ('should hide draft posts from anonymous users' , async () => {
const { userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : `draft-author-${Date .now()} ` ,
});
const { data : insertedPost } = await serviceClient.from ('posts' ).insert ({
author_id : userId,
title : 'Secret Draft' ,
content : 'Not published' ,
published : false ,
}).select ().single ();
const anonClient = createAnonClient ();
const { data } = await anonClient
.from ('posts' )
.select ('*' )
.eq ('id' , insertedPost!.id );
expect (data).toEqual ([]);
});
it ('should allow authors to see their own drafts' , async () => {
const { client, userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : `my-draft-author-${Date .now()} ` ,
});
await serviceClient.from ('posts' ).insert ({
author_id : userId,
title : 'My Draft' ,
content : 'Work in progress' ,
published : false ,
});
const { data } = await client
.from ('posts' )
.select ('*' )
.eq ('author_id' , userId)
.eq ('published' , false );
expect (data!.length ).toBeGreaterThan (0 );
expect (data![0 ].title ).toBe ('My Draft' );
});
it ('should prevent users from creating posts as another author' , async () => {
const { client } = await users.createUser ();
const { userId : otherUserId } = await users.createUser ();
const { error } = await client.from ('posts' ).insert ({
author_id : otherUserId,
title : 'Impersonation Post' ,
content : 'Not my account' ,
});
expect (error).not .toBeNull ();
});
it ('should prevent users from updating other authors posts' , async () => {
const { userId : authorId } = await users.createUser ();
const { client : attackerClient } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : authorId,
username : `update-target-${Date .now()} ` ,
});
const { data : post } = await serviceClient.from ('posts' ).insert ({
author_id : authorId,
title : 'Original Title' ,
content : 'Original content' ,
published : true ,
}).select ().single ();
const { data } = await attackerClient
.from ('posts' )
.update ({ title : 'Hacked Title' })
.eq ('id' , post!.id )
.select ();
expect (data).toEqual ([]);
});
});
Auth Flow Testing
import { describe, it, expect, afterAll } from 'vitest' ;
import { createAnonClient, createServiceClient } from '../helpers/supabase-test-utils' ;
describe ('Supabase Auth Flows' , () => {
const createdUserIds : string [] = [];
afterAll (async () => {
const serviceClient = createServiceClient ();
for (const id of createdUserIds) {
await serviceClient.auth .admin .deleteUser (id).catch (() => {});
}
});
describe ('Email/Password Sign Up' , () => {
it ('should sign up a new user with email and password' , async () => {
const client = createAnonClient ();
const email = `signup-${Date .now()} @test.com` ;
const { data, error } = await client.auth .signUp ({
email,
password : 'TestPassword123!' ,
});
expect (error).toBeNull ();
expect (data.user ).toBeDefined ();
expect (data.user !.email ).toBe (email);
createdUserIds.push (data.user !.id );
});
it ('should reject signup with weak password' , async () => {
const client = createAnonClient ();
const { error } = await client.auth .signUp ({
email : `weak-${Date .now()} @test.com` ,
password : '123' ,
});
expect (error).not .toBeNull ();
});
it ('should reject duplicate email signup' , async () => {
const client = createAnonClient ();
const email = `dup-${Date .now()} @test.com` ;
const { data } = await client.auth .signUp ({
email,
password : 'TestPassword123!' ,
});
createdUserIds.push (data.user !.id );
const { data : dup, error } = await client.auth .signUp ({
email,
password : 'AnotherPassword123!' ,
});
expect (dup.session ).toBeNull ();
});
});
describe ('Email/Password Sign In' , () => {
it ('should sign in with correct credentials' , async () => {
const serviceClient = createServiceClient ();
const email = `signin-${Date .now()} @test.com` ;
const { data : created } = await serviceClient.auth .admin .createUser ({
email,
password : 'TestPassword123!' ,
email_confirm : true ,
});
createdUserIds.push (created.user .id );
const client = createAnonClient ();
const { data, error } = await client.auth .signInWithPassword ({
email,
password : 'TestPassword123!' ,
});
expect (error).toBeNull ();
expect (data.session ).toBeDefined ();
expect (data.session !.access_token ).toBeDefined ();
expect (data.user !.email ).toBe (email);
});
it ('should reject incorrect password' , async () => {
const serviceClient = createServiceClient ();
const email = `wrongpw-${Date .now()} @test.com` ;
const { data : created } = await serviceClient.auth .admin .createUser ({
email,
password : 'CorrectPassword123!' ,
email_confirm : true ,
});
createdUserIds.push (created.user .id );
const client = createAnonClient ();
const { error } = await client.auth .signInWithPassword ({
email,
password : 'WrongPassword123!' ,
});
expect (error).not .toBeNull ();
expect (error!.message ).toContain ('Invalid login credentials' );
});
it ('should reject non-existent email' , async () => {
const client = createAnonClient ();
const { error } = await client.auth .signInWithPassword ({
email : 'doesnotexist@test.com' ,
password : 'TestPassword123!' ,
});
expect (error).not .toBeNull ();
});
});
describe ('Session Management' , () => {
it ('should return a valid session after sign in' , async () => {
const serviceClient = createServiceClient ();
const email = `session-${Date .now()} @test.com` ;
const { data : created } = await serviceClient.auth .admin .createUser ({
email,
password : 'TestPassword123!' ,
email_confirm : true ,
});
createdUserIds.push (created.user .id );
const client = createAnonClient ();
await client.auth .signInWithPassword ({ email, password : 'TestPassword123!' });
const { data : sessionData } = await client.auth .getSession ();
expect (sessionData.session ).not .toBeNull ();
expect (sessionData.session !.expires_at ).toBeDefined ();
});
it ('should sign out and invalidate session' , async () => {
const serviceClient = createServiceClient ();
const email = `signout-${Date .now()} @test.com` ;
const { data : created } = await serviceClient.auth .admin .createUser ({
email,
password : 'TestPassword123!' ,
email_confirm : true ,
});
createdUserIds.push (created.user .id );
const client = createAnonClient ();
await client.auth .signInWithPassword ({ email, password : 'TestPassword123!' });
const { error } = await client.auth .signOut ();
expect (error).toBeNull ();
const { data } = await client.auth .getSession ();
expect (data.session ).toBeNull ();
});
});
describe ('User Metadata' , () => {
it ('should store and retrieve user metadata' , async () => {
const serviceClient = createServiceClient ();
const email = `meta-${Date .now()} @test.com` ;
const { data : created } = await serviceClient.auth .admin .createUser ({
email,
password : 'TestPassword123!' ,
email_confirm : true ,
user_metadata : { role : 'editor' , displayName : 'Test Editor' },
});
createdUserIds.push (created.user .id );
const client = createAnonClient ();
await client.auth .signInWithPassword ({ email, password : 'TestPassword123!' });
const { data } = await client.auth .getUser ();
expect (data.user !.user_metadata .role ).toBe ('editor' );
expect (data.user !.user_metadata .displayName ).toBe ('Test Editor' );
});
it ('should update user metadata' , async () => {
const serviceClient = createServiceClient ();
const email = `update-meta-${Date .now()} @test.com` ;
const { data : created } = await serviceClient.auth .admin .createUser ({
email,
password : 'TestPassword123!' ,
email_confirm : true ,
user_metadata : { displayName : 'Original' },
});
createdUserIds.push (created.user .id );
const client = createAnonClient ();
await client.auth .signInWithPassword ({ email, password : 'TestPassword123!' });
await client.auth .updateUser ({
data : { displayName : 'Updated' },
});
const { data } = await client.auth .getUser ();
expect (data.user !.user_metadata .displayName ).toBe ('Updated' );
});
});
});
Realtime Subscription Testing
import { describe, it, expect, beforeAll, afterAll } from 'vitest' ;
import { createServiceClient } from '../helpers/supabase-test-utils' ;
import { TestUserManager } from '../helpers/test-users' ;
import type { RealtimeChannel } from '@supabase/supabase-js' ;
describe ('Supabase Realtime Subscriptions' , () => {
const users = new TestUserManager ();
let serviceClient : ReturnType <typeof createServiceClient>;
const channels : RealtimeChannel [] = [];
beforeAll (() => {
serviceClient = createServiceClient ();
});
afterAll (async () => {
for (const channel of channels) {
await channel.unsubscribe ();
}
await users.cleanup ();
});
it ('should receive INSERT events on subscribed table' , async () => {
const { client, userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : `realtime-${Date .now()} ` ,
});
const receivedEvents : any [] = [];
const channel = client
.channel ('posts-inserts' )
.on (
'postgres_changes' ,
{ event : 'INSERT' , schema : 'public' , table : 'posts' },
(payload ) => receivedEvents.push (payload)
)
.subscribe ();
channels.push (channel);
await new Promise ((resolve ) => setTimeout (resolve, 1000 ));
await serviceClient.from ('posts' ).insert ({
author_id : userId,
title : 'Realtime Test Post' ,
content : 'Testing realtime' ,
published : true ,
});
await new Promise ((resolve ) => setTimeout (resolve, 2000 ));
expect (receivedEvents.length ).toBeGreaterThan (0 );
expect (receivedEvents[0 ].new .title ).toBe ('Realtime Test Post' );
expect (receivedEvents[0 ].eventType ).toBe ('INSERT' );
});
it ('should receive UPDATE events' , async () => {
const { client, userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : `rt-update-${Date .now()} ` ,
});
const { data : post } = await serviceClient.from ('posts' ).insert ({
author_id : userId,
title : 'Before Update' ,
content : 'Original' ,
published : true ,
}).select ().single ();
const updateEvents : any [] = [];
const channel = client
.channel ('posts-updates' )
.on (
'postgres_changes' ,
{ event : 'UPDATE' , schema : 'public' , table : 'posts' , filter : `id=eq.${post!.id} ` },
(payload ) => updateEvents.push (payload)
)
.subscribe ();
channels.push (channel);
await new Promise ((resolve ) => setTimeout (resolve, 1000 ));
await serviceClient
.from ('posts' )
.update ({ title : 'After Update' })
.eq ('id' , post!.id );
await new Promise ((resolve ) => setTimeout (resolve, 2000 ));
expect (updateEvents.length ).toBeGreaterThan (0 );
expect (updateEvents[0 ].new .title ).toBe ('After Update' );
expect (updateEvents[0 ].old .title ).toBe ('Before Update' );
});
it ('should receive DELETE events' , async () => {
const { client, userId } = await users.createUser ();
await serviceClient.from ('profiles' ).insert ({
id : userId,
username : `rt-delete-${Date .now()} ` ,
});
const { data : post } = await serviceClient.from ('posts' ).insert ({
author_id : userId,
title : 'To Be Deleted' ,
content : 'Bye' ,
published : true ,
}).select ().single ();
const deleteEvents : any [] = [];
const channel = client
.channel ('posts-deletes' )
.on (
'postgres_changes' ,
{ event : 'DELETE' , schema : 'public' , table : 'posts' },
(payload ) => deleteEvents.push (payload)
)
.subscribe ();
channels.push (channel);
await new Promise ((resolve ) => setTimeout (resolve, 1000 ));
await serviceClient.from ('posts' ).delete ().eq ('id' , post!.id );
await new Promise ((resolve ) => setTimeout (resolve, 2000 ));
expect (deleteEvents.length ).toBeGreaterThan (0 );
expect (deleteEvents[0 ].old .id ).toBe (post!.id );
});
it ('should handle channel disconnection gracefully' , async () => {
const { client } = await users.createUser ();
const channel = client
.channel ('disconnect-test' )
.on ('postgres_changes' , { event : '*' , schema : 'public' , table : 'posts' }, () => {})
.subscribe ();
channels.push (channel);
await new Promise ((resolve ) => setTimeout (resolve, 500 ));
const status = await channel.unsubscribe ();
expect (status).toBe ('ok' );
});
});
Storage Bucket Testing
import { describe, it, expect, beforeAll, afterAll } from 'vitest' ;
import { createServiceClient } from '../helpers/supabase-test-utils' ;
import { TestUserManager } from '../helpers/test-users' ;
describe ('Supabase Storage' , () => {
const users = new TestUserManager ();
let serviceClient : ReturnType <typeof createServiceClient>;
beforeAll (async () => {
serviceClient = createServiceClient ();
await serviceClient.storage .createBucket ('avatars' , {
public : false ,
fileSizeLimit : 1024 * 1024 ,
allowedMimeTypes : ['image/png' , 'image/jpeg' , 'image/webp' ],
}).catch (() => {});
});
afterAll (async () => {
await users.cleanup ();
});
it ('should allow authenticated users to upload to their folder' , async () => {
const { client, userId } = await users.createUser ();
const file = new Blob (['fake image data' ], { type : 'image/png' });
const { data, error } = await client.storage
.from ('avatars' )
.upload (`${userId} /avatar.png` , file, {
contentType : 'image/png' ,
});
expect (error).toBeNull ();
expect (data!.path ).toBe (`${userId} /avatar.png` );
});
it ('should prevent users from uploading to other user folders' , async () => {
const { client } = await users.createUser ();
const { userId : otherUserId } = await users.createUser ();
const file = new Blob (['fake data' ], { type : 'image/png' });
const { error } = await client.storage
.from ('avatars' )
.upload (`${otherUserId} /malicious.png` , file, {
contentType : 'image/png' ,
});
expect (error).not .toBeNull ();
});
it ('should enforce file size limits' , async () => {
const { client, userId } = await users.createUser ();
const largeFile = new Blob ([new ArrayBuffer (2 * 1024 * 1024 )], { type : 'image/png' });
const { error } = await client.storage
.from ('avatars' )
.upload (`${userId} /too-large.png` , largeFile);
expect (error).not .toBeNull ();
});
it ('should enforce allowed MIME types' , async () => {
const { client, userId } = await users.createUser ();
const file = new Blob (['not an image' ], { type : 'application/pdf' });
const { error } = await client.storage
.from ('avatars' )
.upload (`${userId} /doc.pdf` , file, {
contentType : 'application/pdf' ,
});
expect (error).not .toBeNull ();
});
it ('should generate signed URLs for private files' , async () => {
const { client, userId } = await users.createUser ();
const file = new Blob (['test data' ], { type : 'image/png' });
await client.storage
.from ('avatars' )
.upload (`${userId} /signed-test.png` , file, { contentType : 'image/png' });
const { data, error } = await client.storage
.from ('avatars' )
.createSignedUrl (`${userId} /signed-test.png` , 60 );
expect (error).toBeNull ();
expect (data!.signedUrl ).toContain ('token=' );
});
});
Edge Function Testing
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' ;
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' ;
serve (async (req) => {
try {
const { name } = await req.json ();
if (!name) {
return new Response (
JSON .stringify ({ error : 'Name is required' }),
{ status : 400 , headers : { 'Content-Type' : 'application/json' } }
);
}
const authHeader = req.headers .get ('Authorization' );
if (!authHeader) {
return new Response (
JSON .stringify ({ error : 'Unauthorized' }),
{ status : 401 , headers : { 'Content-Type' : 'application/json' } }
);
}
const supabase = createClient (
Deno .env .get ('SUPABASE_URL' )!,
Deno .env .get ('SUPABASE_ANON_KEY' )!,
{ global : { headers : { Authorization : authHeader } } }
);
const { data : { user } } = await supabase.auth .getUser ();
return new Response (
JSON .stringify ({ message : `Hello ${name} !` , userId : user?.id }),
{ status : 200 , headers : { 'Content-Type' : 'application/json' } }
);
} catch (error) {
return new Response (
JSON .stringify ({ error : 'Internal error' }),
{ status : 500 , headers : { 'Content-Type' : 'application/json' } }
);
}
});
import { describe, it, expect, beforeAll, afterAll } from 'vitest' ;
import { createTestUser, cleanupTestUser } from '../helpers/supabase-test-utils' ;
const FUNCTIONS_URL = 'http://127.0.0.1:54321/functions/v1' ;
describe ('Edge Function: hello-world' , () => {
let accessToken : string ;
let userId : string ;
beforeAll (async () => {
const user = await createTestUser ();
userId = user.userId ;
const { data } = await user.client .auth .getSession ();
accessToken = data.session !.access_token ;
});
afterAll (async () => {
await cleanupTestUser (userId);
});
it ('should return greeting with user info' , async () => {
const response = await fetch (`${FUNCTIONS_URL} /hello-world` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
Authorization : `Bearer ${accessToken} ` ,
},
body : JSON .stringify ({ name : 'World' }),
});
expect (response.status ).toBe (200 );
const data = await response.json ();
expect (data.message ).toBe ('Hello World!' );
expect (data.userId ).toBe (userId);
});
it ('should return 400 when name is missing' , async () => {
const response = await fetch (`${FUNCTIONS_URL} /hello-world` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
Authorization : `Bearer ${accessToken} ` ,
},
body : JSON .stringify ({}),
});
expect (response.status ).toBe (400 );
const data = await response.json ();
expect (data.error ).toBe ('Name is required' );
});
it ('should return 401 without auth header' , async () => {
const response = await fetch (`${FUNCTIONS_URL} /hello-world` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON .stringify ({ name : 'World' }),
});
expect (response.status ).toBe (401 );
});
});
Database Migration Testing
import { describe, it, expect, beforeAll } from 'vitest' ;
import { createServiceClient } from '../../__tests__/helpers/supabase-test-utils' ;
describe ('Database Migration Verification' , () => {
let serviceClient : ReturnType <typeof createServiceClient>;
beforeAll (() => {
serviceClient = createServiceClient ();
});
it ('should have profiles table with correct columns' , async () => {
const { data, error } = await serviceClient.rpc ('get_table_columns' , {
p_table : 'profiles' ,
});
const { data : columns } = await serviceClient
.from ('information_schema.columns' as any )
.select ('column_name, data_type, is_nullable' )
.eq ('table_name' , 'profiles' )
.eq ('table_schema' , 'public' );
const columnNames = columns!.map ((c : any ) => c.column_name );
expect (columnNames).toContain ('id' );
expect (columnNames).toContain ('username' );
expect (columnNames).toContain ('bio' );
expect (columnNames).toContain ('avatar_url' );
expect (columnNames).toContain ('is_public' );
expect (columnNames).toContain ('created_at' );
});
it ('should have RLS enabled on all tables' , async () => {
const { data : tables } = await serviceClient.rpc ('check_rls_enabled' );
const publicTables = ['profiles' , 'posts' , 'comments' ];
for (const table of publicTables) {
const tableInfo = tables?.find ((t : any ) => t.tablename === table);
expect (tableInfo?.rowsecurity , `RLS not enabled on ${table} ` ).toBe (true );
}
});
it ('should have correct foreign key relationships' , async () => {
const { data : fks } = await serviceClient
.from ('information_schema.referential_constraints' as any )
.select ('constraint_name' )
.eq ('constraint_schema' , 'public' );
expect (fks!.length ).toBeGreaterThan (0 );
});
});
Best Practices
Always use supabase start for local testing -- Never test against production or staging Supabase instances. Local testing is fast, isolated, and free.
Test every RLS policy with at least 3 roles -- For each policy, test as the resource owner, a different authenticated user, and an anonymous user.
Use the service role client for test setup only -- Create test data with the service role client, but run actual tests with anon or authenticated clients to verify RLS.
Clean up test users after each suite -- Use a TestUserManager that tracks created users and deletes them in afterAll to prevent test database bloat.
Disable email confirmation for test environments -- Set enable_confirmations = false in the local config to avoid needing to handle email verification in tests.
Test RLS with both SELECT and the actual operation -- A user may be able to SELECT a row but not UPDATE or DELETE it. Test each operation independently.
Verify RLS failures are silent, not errors -- Supabase RLS violations on UPDATE/DELETE return empty results, not errors. Assert on the returned data being empty.
Test realtime with proper timeouts -- Realtime events have latency. Use adequate waits (1-2 seconds) between subscribing and expecting events.
Use unique identifiers in test data -- Include timestamps or random strings in test usernames and emails to prevent collisions between parallel test runs.
Test storage policies separately from application logic -- Storage bucket policies are independent of table RLS. Write dedicated storage tests.
Anti-Patterns to Avoid
Testing against production Supabase -- Production testing creates real data, costs money, and risks exposing test users. Always use local instances.
Using the service role client in application tests -- The service role bypasses RLS entirely. If your tests use it for data access, they cannot verify security policies.
Not testing RLS at all -- Many teams skip RLS testing because it requires user management. This is the most critical security boundary in a Supabase app.
Sharing test users between test files -- Parallel test execution with shared users causes race conditions. Each test file should create its own users.
Testing only SELECT policies -- INSERT, UPDATE, and DELETE policies have different USING and WITH CHECK clauses. Test each operation type.
Ignoring the difference between USING and WITH CHECK -- USING filters rows you can see, WITH CHECK validates the new row on INSERT/UPDATE. Both need testing.
Hardcoding Supabase keys in test files -- Use environment variables or read from the local Supabase config. Hardcoded keys get committed to version control.
Not testing auth edge cases -- Token expiry, session refresh, and concurrent sessions are common sources of production bugs. Test these flows.
Skipping migration rollback tests -- Only testing the up migration is not enough. Verify that down migrations cleanly reverse schema changes.
Testing realtime without actually subscribing -- Mocking the realtime client misses connection issues, reconnection logic, and message ordering problems.
Running Tests
Start local Supabase: supabase start
Run all tests: pnpm vitest run
Run RLS tests: pnpm vitest run __tests__/integration/rls
Run auth tests: pnpm vitest run __tests__/integration/auth-flow.test.ts
Run realtime tests: pnpm vitest run __tests__/integration/realtime.test.ts
Run edge function tests: supabase functions serve && pnpm vitest run __tests__/integration/edge-functions.test.ts
Reset local database: supabase db reset
View local Supabase dashboard: open http://127.0.0.1:54323
Stop local Supabase: supabase stop