diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aae930b3..59138a39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,12 @@ jobs: if: ${{ matrix.deno == '2.x' }} run: npm run test:integration:browser + - name: Run Edge Functions Tests + if: ${{ matrix.deno == '2.x' }} + run: | + cd test/deno + npm run test:edge-functions + - name: Stop Supabase if: always() run: supabase stop @@ -176,8 +182,13 @@ jobs: npm ci npm run build + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + - name: Run integration tests - run: npm run test:integration || npm run test:integration + run: | + export SUPABASE_SERVICE_ROLE_KEY="$(supabase status --output json | jq -r '.SERVICE_ROLE_KEY')" + npm run test:integration || npm run test:integration - name: Stop Supabase if: always() diff --git a/package.json b/package.json index 1d13d5ad..70f31408 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "test:coverage": "jest --runInBand --coverage --testPathIgnorePatterns=\"test/integration|test/deno\"", "test:integration": "jest --runInBand --detectOpenHandles test/integration.test.ts", "test:integration:browser": "deno test --allow-all test/integration.browser.test.ts", + "test:edge-functions": "deno test --allow-all --no-check test/deno/edge-functions-integration.test.ts", "test:watch": "jest --watch --verbose false --silent false", "test:node:playwright": "cd test/integration/node-browser && npm install && cp ../../../dist/umd/supabase.js . && npm run test", "test:bun": "cd test/integration/bun && bun install && bun test", diff --git a/supabase/functions/echo/index.ts b/supabase/functions/echo/index.ts new file mode 100644 index 00000000..a4b9f85f --- /dev/null +++ b/supabase/functions/echo/index.ts @@ -0,0 +1,33 @@ +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + const body = await req.json() + const data = { + echo: body, + method: req.method, + url: req.url, + timestamp: new Date().toISOString(), + } + + return new Response(JSON.stringify(data), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + }) + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + }) + } +}) diff --git a/supabase/functions/hello/index.ts b/supabase/functions/hello/index.ts new file mode 100644 index 00000000..d72d185d --- /dev/null +++ b/supabase/functions/hello/index.ts @@ -0,0 +1,31 @@ +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + const { name } = await req.json() + const data = { + message: `Hello ${name || 'World'}!`, + timestamp: new Date().toISOString(), + } + + return new Response(JSON.stringify(data), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + }) + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 400, + }) + } +}) diff --git a/supabase/functions/status/index.ts b/supabase/functions/status/index.ts new file mode 100644 index 00000000..fbba08bc --- /dev/null +++ b/supabase/functions/status/index.ts @@ -0,0 +1,33 @@ +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) + } + + try { + const data = { + status: 'ok', + timestamp: new Date().toISOString(), + environment: Deno.env.get('ENVIRONMENT') || 'development', + version: '1.0.0', + uptime: Date.now(), + } + + return new Response(JSON.stringify(data), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 200, + }) + } catch (error) { + return new Response(JSON.stringify({ error: error.message }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 500, + }) + } +}) diff --git a/supabase/migrations/20250422000000_create_todos_table.sql b/supabase/migrations/20250422000000_create_todos_table.sql index a2dd7daa..ca7a76d8 100644 --- a/supabase/migrations/20250422000000_create_todos_table.sql +++ b/supabase/migrations/20250422000000_create_todos_table.sql @@ -3,15 +3,52 @@ CREATE TABLE IF NOT EXISTS public.todos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), task TEXT NOT NULL, is_complete BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + user_id UUID REFERENCES auth.users(id) ); -- Set up Row Level Security (RLS) ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY; --- Create policies -CREATE POLICY "Allow anonymous access to todos" ON public.todos - FOR ALL +-- Allow anonymous users to read all todos (public data) +CREATE POLICY "Allow anonymous read access" ON public.todos + FOR SELECT + TO anon + USING (true); + +-- Allow anonymous users to insert todos (for backward compatibility with old tests) +CREATE POLICY "Allow anonymous insert access" ON public.todos + FOR INSERT TO anon - USING (true) WITH CHECK (true); + +-- Allow anonymous users to delete todos (for backward compatibility with old tests) +CREATE POLICY "Allow anonymous delete access" ON public.todos + FOR DELETE + TO anon + USING (true); + +-- Allow authenticated users to read their own todos +CREATE POLICY "Allow authenticated read own todos" ON public.todos + FOR SELECT + TO authenticated + USING (auth.uid() = user_id); + +-- Allow authenticated users to insert their own todos +CREATE POLICY "Allow authenticated insert own todos" ON public.todos + FOR INSERT + TO authenticated + WITH CHECK (auth.uid() = user_id); + +-- Allow authenticated users to update their own todos +CREATE POLICY "Allow authenticated update own todos" ON public.todos + FOR UPDATE + TO authenticated + USING (auth.uid() = user_id) + WITH CHECK (auth.uid() = user_id); + +-- Allow authenticated users to delete their own todos +CREATE POLICY "Allow authenticated delete own todos" ON public.todos + FOR DELETE + TO authenticated + USING (auth.uid() = user_id); diff --git a/supabase/migrations/20250424000000_storage_anon_policy.sql b/supabase/migrations/20250424000000_storage_anon_policy.sql new file mode 100644 index 00000000..dfea0b66 --- /dev/null +++ b/supabase/migrations/20250424000000_storage_anon_policy.sql @@ -0,0 +1,4 @@ +-- Create test bucket for storage tests +insert into storage.buckets (id, name, public) +values ('test-bucket', 'test-bucket', false) +on conflict (id) do nothing; diff --git a/test/deno/edge-functions-integration.test.ts b/test/deno/edge-functions-integration.test.ts new file mode 100644 index 00000000..861d28ed --- /dev/null +++ b/test/deno/edge-functions-integration.test.ts @@ -0,0 +1,132 @@ +import { assertEquals, assertExists } from 'https://deno.land/std@0.220.1/assert/mod.ts' +import { createClient } from '../../dist/module/index.js' + +// These tests are for integration testing with actual deployed edge functions +// To run these tests, you need to: +// 1. Deploy the edge functions to a Supabase project +// 2. Set the SUPABASE_URL and SUPABASE_ANON_KEY environment variables +// 3. Or use the local development credentials below + +Deno.test( + 'Edge Functions Integration Tests', + { sanitizeOps: false, sanitizeResources: false }, + async (t) => { + // Use environment variables or fall back to local development + const SUPABASE_URL = Deno.env.get('SUPABASE_URL') || 'http://127.0.0.1:54321' + const ANON_KEY = + Deno.env.get('SUPABASE_ANON_KEY') || + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' + + const supabase = createClient(SUPABASE_URL, ANON_KEY, { + realtime: { heartbeatIntervalMs: 500 }, + }) + + try { + await t.step('hello function - should return greeting with name', async () => { + const { data, error } = await supabase.functions.invoke('hello', { + body: { name: 'Test User' }, + }) + + assertEquals(error, null) + assertExists(data) + assertEquals(typeof data.message, 'string') + assertEquals(data.message, 'Hello Test User!') + assertEquals(typeof data.timestamp, 'string') + }) + + await t.step('hello function - should return default greeting without name', async () => { + const { data, error } = await supabase.functions.invoke('hello', { + body: {}, + }) + + assertEquals(error, null) + assertExists(data) + assertEquals(typeof data.message, 'string') + assertEquals(data.message, 'Hello World!') + assertEquals(typeof data.timestamp, 'string') + }) + + await t.step('echo function - should echo request body', async () => { + const testData = { + message: 'Hello Echo!', + number: 42, + array: [1, 2, 3], + nested: { key: 'value' }, + } + + const { data, error } = await supabase.functions.invoke('echo', { + body: testData, + }) + + assertEquals(error, null) + assertExists(data) + assertEquals(data.echo, testData) + assertEquals(typeof data.method, 'string') + assertEquals(typeof data.url, 'string') + assertEquals(typeof data.timestamp, 'string') + }) + + await t.step('status function - should return system status', async () => { + const { data, error } = await supabase.functions.invoke('status', { + body: {}, + }) + + assertEquals(error, null) + assertExists(data) + assertEquals(data.status, 'ok') + assertEquals(typeof data.timestamp, 'string') + assertEquals(typeof data.environment, 'string') + assertEquals(data.version, '1.0.0') + assertEquals(typeof data.uptime, 'number') + }) + + await t.step('should handle non-existent function', async () => { + const { data, error } = await supabase.functions.invoke('non-existent-function', { + body: {}, + }) + + assertExists(error) + assertEquals(data, null) + }) + + await t.step('should handle concurrent function calls', async () => { + const promises = Array.from({ length: 5 }, (_, i) => + supabase.functions.invoke('hello', { + body: { name: `Concurrent Test ${i}` }, + }) + ) + + const results = await Promise.all(promises) + + // Check if any functions are deployed + const hasDeployedFunctions = results.some(({ error }) => !error) + + if (!hasDeployedFunctions) { + console.log('No functions deployed, skipping concurrent execution test') + return + } + + results.forEach(({ data, error }) => { + if (!error) { + assertEquals(error, null) + assertExists(data) + assertEquals(typeof data.message, 'string') + assertEquals(typeof data.timestamp, 'string') + } + }) + }) + + await t.step('should handle function errors gracefully', async () => { + const { data, error } = await supabase.functions.invoke('hello', { + body: 'invalid json', + }) + + assertExists(error) + assertEquals(data, null) + }) + } catch (error) { + console.error('Test error:', error) + throw error + } + } +) diff --git a/test/deno/integration.test.ts b/test/deno/integration.test.ts index ca06e275..4107ddb7 100644 --- a/test/deno/integration.test.ts +++ b/test/deno/integration.test.ts @@ -88,6 +88,64 @@ Deno.test( assertEquals(data.user!.email, email) }) + await t.step('Authentication - should sign in and out successfully', async () => { + const email = `deno-signout-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + const { data, error } = await supabase.auth.signInWithPassword({ email, password }) + + assertEquals(error, null) + assertExists(data.user) + assertEquals(data.user!.email, email) + + const { error: signOutError } = await supabase.auth.signOut() + + assertEquals(signOutError, null) + }) + + await t.step('Authentication - should get current user', async () => { + const email = `deno-getuser-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + await supabase.auth.signInWithPassword({ email, password }) + + const { data, error } = await supabase.auth.getUser() + + assertEquals(error, null) + assertExists(data.user) + assertEquals(data.user!.email, email) + }) + + await t.step('Authentication - should handle invalid credentials', async () => { + const email = `deno-invalid-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password: 'wrongpassword', + }) + + assertExists(error) + assertEquals(data.user, null) + }) + + await t.step('Authentication - should handle non-existent user', async () => { + const email = `deno-nonexistent-${Date.now()}@example.com` + const password = 'password123' + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + + assertExists(error) + assertEquals(data.user, null) + }) + await t.step('Realtime - is able to connect and broadcast', async () => { const channelName = `channel-${crypto.randomUUID()}` let channel: RealtimeChannel @@ -139,6 +197,43 @@ Deno.test( // Cleanup channel await channel.unsubscribe() }) + + await t.step('Storage - should upload and list file in bucket', async () => { + const bucket = 'test-bucket' + const filePath = 'test-file.txt' + const fileContent = new Blob(['Hello, Supabase Storage!'], { type: 'text/plain' }) + + // use service_role key for bypass RLS + const SERVICE_ROLE_KEY = + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU' + const supabaseWithServiceRole = createClient(SUPABASE_URL, SERVICE_ROLE_KEY, { + realtime: { heartbeatIntervalMs: 500 }, + }) + + // upload + const { data: uploadData, error: uploadError } = await supabaseWithServiceRole.storage + .from(bucket) + .upload(filePath, fileContent, { upsert: true }) + assertEquals(uploadError, null) + assertExists(uploadData) + + // list + const { data: listData, error: listError } = await supabaseWithServiceRole.storage + .from(bucket) + .list() + assertEquals(listError, null) + assertEquals(Array.isArray(listData), true) + if (!listData) throw new Error('listData is null') + const fileNames = listData.map((f: any) => f.name) + assertEquals(fileNames.includes('test-file.txt'), true) + + // delete file + const { error: deleteError } = await supabaseWithServiceRole.storage + .from(bucket) + .remove([filePath]) + assertEquals(deleteError, null) + }) } finally { // Ensure cleanup runs even if tests fail await cleanup() diff --git a/test/deno/package.json b/test/deno/package.json index a815e277..10d6397d 100644 --- a/test/deno/package.json +++ b/test/deno/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "test": "npm run setup-deps && deno test --allow-all --unstable-sloppy-imports integration.test.ts", + "test:edge-functions": "npm run setup-deps && deno test --allow-all --no-check --unstable-sloppy-imports edge-functions-integration.test.ts", "setup-deps": "node setup-deps.js" }, "dependencies": { diff --git a/test/integration.test.ts b/test/integration.test.ts index b3862416..227b3890 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -2,23 +2,23 @@ import { createClient, RealtimeChannel, SupabaseClient } from '../src/index' // These tests assume that a local Supabase server is already running // Start a local Supabase instance with 'supabase start' before running these tests -describe('Supabase Integration Tests', () => { - // Default local dev credentials from Supabase CLI - const SUPABASE_URL = 'http://127.0.0.1:54321' - const ANON_KEY = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' +// Default local dev credentials from Supabase CLI +const SUPABASE_URL = 'http://127.0.0.1:54321' +const ANON_KEY = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' - const supabase = createClient(SUPABASE_URL, ANON_KEY, { - realtime: { heartbeatIntervalMs: 500 }, - }) +const supabase = createClient(SUPABASE_URL, ANON_KEY, { + realtime: { heartbeatIntervalMs: 500 }, +}) +describe('Supabase Integration Tests', () => { test('should connect to Supabase instance', async () => { expect(supabase).toBeDefined() expect(supabase).toBeInstanceOf(SupabaseClient) }) describe('PostgREST', () => { - test('should query data from public schema', async () => { + test('should connect to PostgREST API', async () => { const { data, error } = await supabase.from('todos').select('*').limit(5) // The default schema includes a 'todos' table, but it might be empty @@ -57,11 +57,126 @@ describe('Supabase Integration Tests', () => { }) }) + describe('PostgreSQL RLS', () => { + let user1Email: string + let user2Email: string + let user1Id: string + let user2Id: string + let user1TodoId: string + let user2TodoId: string + + beforeAll(async () => { + // Create two test users + user1Email = `user1-${Date.now()}@example.com` + user2Email = `user2-${Date.now()}@example.com` + const password = 'password123' + + const { data: user1Data } = await supabase.auth.signUp({ + email: user1Email, + password, + }) + user1Id = user1Data.user!.id + + const { data: user2Data } = await supabase.auth.signUp({ + email: user2Email, + password, + }) + user2Id = user2Data.user!.id + + // Create todos for both users + await supabase.auth.signInWithPassword({ email: user1Email, password }) + const { data: user1Todo } = await supabase + .from('todos') + .insert({ task: 'User 1 Todo', is_complete: false, user_id: user1Id }) + .select() + .single() + user1TodoId = user1Todo!.id + + await supabase.auth.signInWithPassword({ email: user2Email, password }) + const { data: user2Todo } = await supabase + .from('todos') + .insert({ task: 'User 2 Todo', is_complete: false, user_id: user2Id }) + .select() + .single() + user2TodoId = user2Todo!.id + }) + + afterAll(async () => { + await supabase.auth.signOut() + }) + + test('should allow anonymous access via RLS policies', async () => { + await supabase.auth.signOut() + + const { data, error } = await supabase.from('todos').select('*').limit(5) + + expect(error).toBeNull() + expect(Array.isArray(data)).toBe(true) + }) + + test('should allow authenticated user to access their own data', async () => { + await supabase.auth.signInWithPassword({ email: user1Email, password: 'password123' }) + + const { data, error } = await supabase + .from('todos') + .select('*') + .eq('id', user1TodoId) + .single() + + expect(error).toBeNull() + expect(data).toBeDefined() + expect(data!.task).toBe('User 1 Todo') + }) + + test('should prevent access to other users data', async () => { + await supabase.auth.signInWithPassword({ email: user1Email, password: 'password123' }) + + const { data, error } = await supabase + .from('todos') + .select('*') + .eq('id', user2TodoId) + .single() + + expect(error).not.toBeNull() + expect(data).toBeNull() + }) + + test('should allow authenticated user to create their own data', async () => { + await supabase.auth.signInWithPassword({ email: user1Email, password: 'password123' }) + + const { data, error } = await supabase + .from('todos') + .insert({ task: 'New User 1 Todo', is_complete: false, user_id: user1Id }) + .select() + .single() + + expect(error).toBeNull() + expect(data).toBeDefined() + expect(data!.task).toBe('New User 1 Todo') + }) + + test('should allow authenticated user to update their own data', async () => { + await supabase.auth.signInWithPassword({ email: user1Email, password: 'password123' }) + + const { data, error } = await supabase + .from('todos') + .update({ task: 'Updated User 1 Todo' }) + .eq('id', user1TodoId) + .select() + .single() + + expect(error).toBeNull() + expect(data).toBeDefined() + expect(data!.task).toBe('Updated User 1 Todo') + }) + }) + describe('Authentication', () => { afterAll(async () => { // Clean up by signing out the user await supabase.auth.signOut() }) + test('should sign up a user', async () => { const email = `test-${Date.now()}@example.com` const password = 'password123' @@ -75,6 +190,64 @@ describe('Supabase Integration Tests', () => { expect(data.user).toBeDefined() expect(data.user!.email).toBe(email) }) + + test('should sign in and out successfully', async () => { + const email = `test-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + const { data, error } = await supabase.auth.signInWithPassword({ email, password }) + + expect(error).toBeNull() + expect(data.user).toBeDefined() + expect(data.user!.email).toBe(email) + + const { error: signOutError } = await supabase.auth.signOut() + + expect(signOutError).toBeNull() + }) + + test('should get current user', async () => { + const email = `test-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + await supabase.auth.signInWithPassword({ email, password }) + + const { data, error } = await supabase.auth.getUser() + + expect(error).toBeNull() + expect(data.user).toBeDefined() + expect(data.user!.email).toBe(email) + }) + + test('should handle invalid credentials', async () => { + const email = `test-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password: 'wrongpassword', + }) + + expect(error).not.toBeNull() + expect(data.user).toBeNull() + }) + + test('should handle non-existent user', async () => { + const email = `nonexistent-${Date.now()}@example.com` + const password = 'password123' + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + + expect(error).not.toBeNull() + expect(data.user).toBeNull() + }) }) describe('Realtime', () => { @@ -133,3 +306,40 @@ describe('Supabase Integration Tests', () => { }, 10000) }) }) + +describe('Storage API', () => { + const bucket = 'test-bucket' + const filePath = 'test-file.txt' + const fileContent = new Blob(['Hello, Supabase Storage!'], { type: 'text/plain' }) + + // use service_role key for bypass RLS + const SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || 'use-service-role-key' + const supabaseWithServiceRole = createClient(SUPABASE_URL, SERVICE_ROLE_KEY, { + realtime: { heartbeatIntervalMs: 500 }, + }) + + test('upload and list file in bucket', async () => { + // upload + const { data: uploadData, error: uploadError } = await supabaseWithServiceRole.storage + .from(bucket) + .upload(filePath, fileContent, { upsert: true }) + expect(uploadError).toBeNull() + expect(uploadData).toBeDefined() + + // list + const { data: listData, error: listError } = await supabaseWithServiceRole.storage + .from(bucket) + .list() + expect(listError).toBeNull() + expect(Array.isArray(listData)).toBe(true) + if (!listData) throw new Error('listData is null') + const fileNames = listData.map((f: any) => f.name) + expect(fileNames).toContain('test-file.txt') + + // delete file + const { error: deleteError } = await supabaseWithServiceRole.storage + .from(bucket) + .remove([filePath]) + expect(deleteError).toBeNull() + }) +}) diff --git a/test/integration/bun/integration.test.ts b/test/integration/bun/integration.test.ts index 608774b4..985a172c 100644 --- a/test/integration/bun/integration.test.ts +++ b/test/integration/bun/integration.test.ts @@ -1,16 +1,15 @@ import { test, expect } from 'bun:test' import { createClient } from '@supabase/supabase-js' -test('should subscribe to realtime channel', async () => { - const SUPABASE_URL = 'http://127.0.0.1:54321' - const ANON_KEY = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' +const SUPABASE_URL = 'http://127.0.0.1:54321' +const ANON_KEY = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' - const supabase = createClient(SUPABASE_URL, ANON_KEY, { - realtime: { heartbeatIntervalMs: 500 }, - }) +const supabase = createClient(SUPABASE_URL, ANON_KEY, { + realtime: { heartbeatIntervalMs: 500 }, +}) - // Setup authentication +test('should subscribe to realtime channel', async () => { await supabase.auth.signOut() const email = `bun-test-${Date.now()}@example.com` const password = 'password123' @@ -43,3 +42,107 @@ test('should subscribe to realtime channel', async () => { // Cleanup await supabase.removeAllChannels() }, 10000) + +test('should sign up a user', async () => { + await supabase.auth.signOut() + const email = `bun-auth-${Date.now()}@example.com` + const password = 'password123' + + const { data, error } = await supabase.auth.signUp({ + email, + password, + }) + + expect(error).toBeNull() + expect(data.user).toBeDefined() + expect(data.user!.email).toBe(email) +}) + +test('should sign in and out successfully', async () => { + await supabase.auth.signOut() + const email = `bun-signin-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + + expect(error).toBeNull() + expect(data.user).toBeDefined() + expect(data.user!.email).toBe(email) + + const { error: signOutError } = await supabase.auth.signOut() + + expect(signOutError).toBeNull() +}) + +test('should get current user', async () => { + await supabase.auth.signOut() + const email = `bun-getuser-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + await supabase.auth.signInWithPassword({ email, password }) + + const { data, error } = await supabase.auth.getUser() + + expect(error).toBeNull() + expect(data.user).toBeDefined() + expect(data.user!.email).toBe(email) +}) + +test('should handle invalid credentials', async () => { + await supabase.auth.signOut() + const email = `bun-invalid-${Date.now()}@example.com` + const password = 'password123' + + await supabase.auth.signUp({ email, password }) + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password: 'wrongpassword', + }) + + expect(error).not.toBeNull() + expect(data.user).toBeNull() +}) + +test('should upload and list file in bucket', async () => { + const bucket = 'test-bucket' + const filePath = 'test-file.txt' + const fileContent = new Blob(['Hello, Supabase Storage!'], { type: 'text/plain' }) + + // use service_role key for bypass RLS + const SERVICE_ROLE_KEY = + process.env.SUPABASE_SERVICE_ROLE_KEY || + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU' + const supabaseWithServiceRole = createClient(SUPABASE_URL, SERVICE_ROLE_KEY, { + realtime: { heartbeatIntervalMs: 500 }, + }) + + // upload + const { data: uploadData, error: uploadError } = await supabaseWithServiceRole.storage + .from(bucket) + .upload(filePath, fileContent, { upsert: true }) + expect(uploadError).toBeNull() + expect(uploadData).toBeDefined() + + // list + const { data: listData, error: listError } = await supabaseWithServiceRole.storage + .from(bucket) + .list() + expect(listError).toBeNull() + expect(Array.isArray(listData)).toBe(true) + if (!listData) throw new Error('listData is null') + const fileNames = listData.map((f: any) => f.name) + expect(fileNames).toContain('test-file.txt') + + // delete file + const { error: deleteError } = await supabaseWithServiceRole.storage + .from(bucket) + .remove([filePath]) + expect(deleteError).toBeNull() +}) diff --git a/test/integration/next/playwright.config.ts b/test/integration/next/playwright.config.ts index 75d53188..001172cf 100644 --- a/test/integration/next/playwright.config.ts +++ b/test/integration/next/playwright.config.ts @@ -4,13 +4,14 @@ export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, + retries: 0, workers: process.env.CI ? 1 : undefined, + timeout: 20000, reporter: 'html', use: { baseURL: 'http://localhost:3000', - trace: 'on-first-retry', - video: 'on-first-retry', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', }, projects: [ { diff --git a/test/integration/node-browser/playwright.config.ts b/test/integration/node-browser/playwright.config.ts index b42dd367..33223a5e 100644 --- a/test/integration/node-browser/playwright.config.ts +++ b/test/integration/node-browser/playwright.config.ts @@ -4,12 +4,14 @@ export default defineConfig({ testDir: './', fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, + retries: 0, workers: process.env.CI ? 1 : undefined, + timeout: 20000, reporter: 'html', use: { baseURL: 'http://localhost:8004', - trace: 'on-first-retry', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', }, projects: [ {