361 lines
11 KiB
TypeScript
Executable File
361 lines
11 KiB
TypeScript
Executable File
import { createClient, RealtimeChannel, SupabaseClient } from '../src/index.ts'
|
|
|
|
// These tests assume that a local Supabase server is already running
|
|
// Start a local Supabase instance with 'supabase start' before running these tests
|
|
// Default local dev credentials from Supabase CLI
|
|
const SUPABASE_URL = 'http://127.0.0.1:54321'
|
|
const ANON_KEY =
|
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0'
|
|
|
|
// For Node.js < 22, we need to provide a WebSocket implementation
|
|
// Node.js 22+ has native WebSocket support
|
|
let wsTransport: any = undefined
|
|
if (typeof WebSocket === 'undefined' && typeof process !== 'undefined' && process.versions?.node) {
|
|
try {
|
|
wsTransport = require('./ws')
|
|
} catch (error) {
|
|
console.warn('WebSocket not available, Realtime features may not work')
|
|
}
|
|
}
|
|
|
|
const supabase = createClient(SUPABASE_URL, ANON_KEY, {
|
|
realtime: {
|
|
heartbeatIntervalMs: 500,
|
|
...(wsTransport && { transport: wsTransport }),
|
|
},
|
|
})
|
|
|
|
describe('Supabase Integration Tests', () => {
|
|
test('should connect to Supabase instance', async () => {
|
|
expect(supabase).toBeDefined()
|
|
expect(supabase).toBeInstanceOf(SupabaseClient)
|
|
})
|
|
|
|
describe('PostgREST', () => {
|
|
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
|
|
expect(error).toBeNull()
|
|
expect(Array.isArray(data)).toBe(true)
|
|
})
|
|
|
|
// Test creating and deleting data
|
|
test('should create and delete a todo', async () => {
|
|
// Create a new todo
|
|
const { data: createdTodo, error: createError } = await supabase
|
|
.from('todos')
|
|
.insert({ task: 'Integration Test Todo', is_complete: false })
|
|
.select()
|
|
.single()
|
|
|
|
expect(createError).toBeNull()
|
|
expect(createdTodo).toBeDefined()
|
|
expect(createdTodo!.task).toBe('Integration Test Todo')
|
|
expect(createdTodo!.is_complete).toBe(false)
|
|
|
|
// Delete the created todo
|
|
const { error: deleteError } = await supabase.from('todos').delete().eq('id', createdTodo!.id)
|
|
|
|
expect(deleteError).toBeNull()
|
|
|
|
// Verify the todo was deleted
|
|
const { data: fetchedTodo, error: fetchError } = await supabase
|
|
.from('todos')
|
|
.select('*')
|
|
.eq('id', createdTodo!.id)
|
|
.single()
|
|
|
|
expect(fetchError).not.toBeNull()
|
|
expect(fetchedTodo).toBeNull()
|
|
})
|
|
})
|
|
|
|
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'
|
|
|
|
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 () => {
|
|
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', () => {
|
|
const channelName = `channel-${crypto.randomUUID()}`
|
|
let channel: RealtimeChannel
|
|
let email: string
|
|
let password: string
|
|
|
|
beforeEach(async () => {
|
|
await supabase.auth.signOut()
|
|
email = `test-${Date.now()}@example.com`
|
|
password = 'password123'
|
|
await supabase.auth.signUp({ email, password })
|
|
|
|
const config = { broadcast: { self: true }, private: true }
|
|
channel = supabase.channel(channelName, { config })
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await supabase.removeAllChannels()
|
|
})
|
|
|
|
test('is able to connect and broadcast', async () => {
|
|
const testMessage = { message: 'test' }
|
|
let receivedMessage: any
|
|
let subscribed = false
|
|
let attempts = 0
|
|
|
|
channel
|
|
.on('broadcast', { event: '*' }, (payload) => (receivedMessage = payload))
|
|
.subscribe((status) => {
|
|
if (status == 'SUBSCRIBED') subscribed = true
|
|
})
|
|
|
|
// Wait for subscription
|
|
while (!subscribed) {
|
|
if (attempts > 50) throw new Error('Timeout waiting for subscription')
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
attempts++
|
|
}
|
|
|
|
attempts = 0
|
|
|
|
channel.send({ type: 'broadcast', event: 'test-event', payload: testMessage })
|
|
|
|
// Wait on message
|
|
while (!receivedMessage) {
|
|
if (attempts > 50) throw new Error('Timeout waiting for message')
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
attempts++
|
|
}
|
|
expect(receivedMessage).toBeDefined()
|
|
expect(supabase.realtime.getChannels().length).toBe(1)
|
|
}, 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,
|
|
...(wsTransport && { transport: wsTransport }),
|
|
},
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|