Files
madbase/tests/integration/storage.test.ts

144 lines
5.4 KiB
TypeScript

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);
});
});
});