added more support for supabase-js
This commit is contained in:
@@ -1,39 +1,143 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { createAnonClient, createServiceRoleClient } from './setup.ts';
|
||||
|
||||
const client = createAnonClient();
|
||||
const admin = createServiceRoleClient();
|
||||
const bucket = 'test-bucket';
|
||||
|
||||
const PUBLIC_BUCKET = 'public-bucket';
|
||||
const PRIVATE_BUCKET = 'private-bucket';
|
||||
|
||||
describe('Storage', () => {
|
||||
it('should upload a file', async () => {
|
||||
// Use Buffer for Node environment reliability
|
||||
const file = Buffer.from('Hello, MadBase!');
|
||||
// Use admin to bypass RLS/Permission issues for now to verify S3 connectivity
|
||||
const { data, error } = await admin.storage
|
||||
.from(bucket)
|
||||
.upload('hello.txt', file, { upsert: true });
|
||||
const fileName = `hello-${Date.now()}.txt`;
|
||||
const fileContent = Buffer.from('Hello, MadBase!');
|
||||
|
||||
if (error) console.error('Upload error:', error);
|
||||
it('should list buckets', async () => {
|
||||
const { data, error } = await client.storage.listBuckets();
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeDefined();
|
||||
expect(data?.path).toBe('hello.txt');
|
||||
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);
|
||||
});
|
||||
|
||||
it('should list files', async () => {
|
||||
const { data, error } = await client.storage.from(bucket).list();
|
||||
describe('Public Bucket', () => {
|
||||
it('should allow anon to list files', async () => {
|
||||
const { error } = await client.storage.from(PUBLIC_BUCKET).list();
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeDefined();
|
||||
expect(data?.some((f) => f.name === 'hello.txt')).toBe(true);
|
||||
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!');
|
||||
});
|
||||
});
|
||||
|
||||
it('should download a file', async () => {
|
||||
const { data, error } = await client.storage.from(bucket).download('hello.txt');
|
||||
describe('Private Bucket', () => {
|
||||
const privateFile = `secret-${Date.now()}.txt`;
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeDefined();
|
||||
const text = await data?.text();
|
||||
expect(text).toBe('Hello, MadBase!');
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user