import { describe, it, expect, beforeAll } from 'vitest'; import { createAnonClient, createServiceRoleClient } from './setup.ts'; const client = createAnonClient(); const admin = createServiceRoleClient(); const PUBLIC_BUCKET = 'public-bucket'; const PRIVATE_BUCKET = 'private-bucket'; describe('Storage', () => { const fileName = `hello-${Date.now()}.txt`; const fileContent = Buffer.from('Hello, MadBase!'); it('should list buckets', async () => { const { data, error } = await client.storage.listBuckets(); expect(error).toBeNull(); expect(data).toBeDefined(); expect(data?.some((b) => b.name === PUBLIC_BUCKET)).toBe(true); // Private buckets might be visible in list depending on RLS, usually they are if user has access. // But anon might only see public ones if we restricted list policy? // Our migration says: "Public Buckets are viewable by everyone" using (public=true). // So anon should NOT see private bucket. expect(data?.some((b) => b.name === PRIVATE_BUCKET)).toBe(false); }); describe('Public Bucket', () => { it('should allow anon to list files', async () => { const { error } = await client.storage.from(PUBLIC_BUCKET).list(); expect(error).toBeNull(); }); it('should allow upload (via policy)', async () => { const { data, error } = await client.storage .from(PUBLIC_BUCKET) .upload(fileName, fileContent); expect(error).toBeNull(); expect(data?.path).toBe(fileName); }); it('should allow download', async () => { const { data, error } = await client.storage .from(PUBLIC_BUCKET) .download(fileName); expect(error).toBeNull(); const text = await data?.text(); expect(text).toBe('Hello, MadBase!'); }); }); describe('Private Bucket', () => { const privateFile = `secret-${Date.now()}.txt`; it('should NOT allow anon to list files', async () => { // Policy: "Users can view their own buckets" OR "Public Buckets". // Anon is not owner (owner is usually null or specific user). // If bucket is not public, anon shouldn't see it or its objects. // List objects checks: bucket_id IN (SELECT id FROM buckets WHERE public=true) OR owner = sub. const { data, error } = await client.storage.from(PRIVATE_BUCKET).list(); // It might return empty list or error depending on implementation // Supabase storage usually returns empty list if no access to objects, or error if bucket not found/accessible. // Our handler checks bucket existence first. // Bucket exists, but RLS on buckets table filters it out for anon? // `list_objects` handler does: // `SELECT id FROM storage.buckets WHERE id = $1` // If RLS hides it, it returns None -> "Bucket not found" or just "Not Found" if axum returns 404. expect(error).toBeDefined(); expect(error?.message).toContain('Not Found'); }); it('should allow admin (service role) to upload', async () => { const { data, error } = await admin.storage .from(PRIVATE_BUCKET) .upload(privateFile, fileContent); expect(error).toBeNull(); expect(data?.path).toBe(privateFile); }); it('should NOT allow anon to download', async () => { const { data, error } = await client.storage .from(PRIVATE_BUCKET) .download(privateFile); expect(error).toBeDefined(); expect(data).toBeNull(); }); it('should allow admin to download', async () => { const { data, error } = await admin.storage .from(PRIVATE_BUCKET) .download(privateFile); expect(error).toBeNull(); const text = await data?.text(); expect(text).toBe('Hello, MadBase!'); }); }); describe('Signed URLs', () => { const privateFile = `signed-secret-${Date.now()}.txt`; const fileContent = Buffer.from('Hello, MadBase!'); beforeAll(async () => { // Upload a private file as admin const { error } = await admin.storage .from(PRIVATE_BUCKET) .upload(privateFile, fileContent); expect(error).toBeNull(); }); it('should generate and use a signed URL', async () => { // 1. Generate Signed URL (as admin who has access) const { data, error } = await admin.storage .from(PRIVATE_BUCKET) .createSignedUrl(privateFile, 60); expect(error).toBeNull(); expect(data?.signedUrl).toBeDefined(); // 2. Access the file using the signed URL (without auth headers) // The signedUrl from supabase-js might be relative or absolute depending on client config. // Our backend returns relative path: /storage/v1/object/sign/... // So we prepend the API URL. // Note: Supabase JS might construct the full URL if `signedUrl` is returned as path. // Let's inspect what we get. console.log('Signed URL:', data?.signedUrl); const url = data?.signedUrl.startsWith('http') ? data?.signedUrl : `${process.env.MADBASE_URL}${data?.signedUrl}`; const res = await fetch(url); expect(res.status).toBe(200); const text = await res.text(); expect(text).toBe('Hello, MadBase!'); }); it('should fail with invalid token', async () => { const url = `${process.env.MADBASE_URL}/storage/v1/object/sign/${PRIVATE_BUCKET}/${privateFile}?token=invalid-token`; const res = await fetch(url); expect(res.status).toBe(403); }); }); });