144 lines
5.4 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|