added initial roadmap and implementation
This commit is contained in:
42
tests/integration/auth.test.ts
Normal file
42
tests/integration/auth.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createAnonClient } from './setup.ts';
|
||||
|
||||
const client = createAnonClient();
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
const password = 'password123';
|
||||
|
||||
describe('Authentication', () => {
|
||||
it('should sign up a new user', async () => {
|
||||
const { data, error } = await client.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(data.user).toBeDefined();
|
||||
expect(data.user?.email).toBe(email);
|
||||
expect(data.session).toBeDefined(); // Assuming auto-sign-in on signup
|
||||
});
|
||||
|
||||
it('should sign in an existing user', async () => {
|
||||
const { data, error } = await client.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(data.session).toBeDefined();
|
||||
expect(data.user).toBeDefined();
|
||||
expect(data.user?.email).toBe(email);
|
||||
});
|
||||
|
||||
it('should fail with incorrect password', async () => {
|
||||
const { data, error } = await client.auth.signInWithPassword({
|
||||
email,
|
||||
password: 'wrongpassword',
|
||||
});
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(data.session).toBeNull();
|
||||
});
|
||||
});
|
||||
82
tests/integration/db.test.ts
Normal file
82
tests/integration/db.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createAnonClient, createServiceRoleClient } from './setup.ts';
|
||||
|
||||
const client = createAnonClient();
|
||||
const adminClient = createServiceRoleClient();
|
||||
|
||||
describe('Data API (PostgREST-lite)', () => {
|
||||
const todoTitle = `Task ${Date.now()}`;
|
||||
|
||||
it('should create a todo', async () => {
|
||||
const { data: rows, error } = await client
|
||||
.from('todos')
|
||||
.insert({ title: todoTitle, completed: false })
|
||||
.select();
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(rows).toBeDefined();
|
||||
expect(rows?.length).toBe(1);
|
||||
|
||||
const data = rows![0];
|
||||
expect(data.title).toBe(todoTitle);
|
||||
expect(data.completed).toBe(false);
|
||||
});
|
||||
|
||||
it('should list todos', async () => {
|
||||
const { data, error } = await client.from('todos').select('*');
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeDefined();
|
||||
expect(Array.isArray(data)).toBe(true);
|
||||
expect(data?.length).toBeGreaterThan(0);
|
||||
expect(data?.some((t) => t.title === todoTitle)).toBe(true);
|
||||
});
|
||||
|
||||
it('should update a todo', async () => {
|
||||
// First get the todo
|
||||
const { data: todos } = await client
|
||||
.from('todos')
|
||||
.select('id')
|
||||
.eq('title', todoTitle)
|
||||
.limit(1);
|
||||
|
||||
expect(todos).toBeDefined();
|
||||
if (!todos || todos.length === 0) throw new Error('Todo not found');
|
||||
const id = todos[0].id;
|
||||
|
||||
const { error } = await client
|
||||
.from('todos')
|
||||
.update({ completed: true })
|
||||
.eq('id', id);
|
||||
|
||||
expect(error).toBeNull();
|
||||
|
||||
// Verify update
|
||||
const { data: rows } = await client.from('todos').select('*').eq('id', id);
|
||||
expect(rows).toBeDefined();
|
||||
expect(rows?.length).toBe(1);
|
||||
const updated = rows![0];
|
||||
expect(updated.completed).toBe(true);
|
||||
});
|
||||
|
||||
it('should delete a todo', async () => {
|
||||
// First get the todo
|
||||
const { data: todos } = await client
|
||||
.from('todos')
|
||||
.select('id')
|
||||
.eq('title', todoTitle)
|
||||
.limit(1);
|
||||
|
||||
expect(todos).toBeDefined();
|
||||
if (!todos || todos.length === 0) throw new Error('Todo not found');
|
||||
const id = todos[0].id;
|
||||
|
||||
const { error } = await client.from('todos').delete().eq('id', id);
|
||||
|
||||
expect(error).toBeNull();
|
||||
|
||||
// Verify deletion
|
||||
const { data } = await client.from('todos').select('*').eq('id', id);
|
||||
expect(data?.length).toBe(0);
|
||||
});
|
||||
});
|
||||
1646
tests/integration/package-lock.json
generated
Normal file
1646
tests/integration/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
tests/integration/package.json
Normal file
18
tests/integration/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "integration",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.49.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"vitest": "^3.0.7"
|
||||
}
|
||||
}
|
||||
38
tests/integration/realtime.test.ts
Normal file
38
tests/integration/realtime.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createAnonClient } from './setup.ts';
|
||||
|
||||
const client = createAnonClient();
|
||||
|
||||
describe('Realtime', () => {
|
||||
it('should receive insert events', async () => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const channel = client
|
||||
.channel('public:todos')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'todos' },
|
||||
(payload) => {
|
||||
console.log('Received INSERT event:', payload);
|
||||
expect(payload.new).toBeDefined();
|
||||
expect(payload.new.title).toBe('Realtime Test');
|
||||
client.removeChannel(channel).then(() => resolve());
|
||||
}
|
||||
)
|
||||
.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
// Trigger an insert
|
||||
const { error } = await client
|
||||
.from('todos')
|
||||
.insert({ title: 'Realtime Test', completed: false });
|
||||
|
||||
if (error) reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout if no event received
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout waiting for Realtime event'));
|
||||
}, 10000);
|
||||
});
|
||||
}, 10000);
|
||||
});
|
||||
12
tests/integration/run_tests.sh
Executable file
12
tests/integration/run_tests.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Setup Database
|
||||
echo "Setting up test database (applying migrations)..."
|
||||
# Concatenate all migrations and setup script
|
||||
cat migrations/*.sql tests/integration/setup_db.sql | podman exec -i madbase_db psql -U postgres -d postgres
|
||||
|
||||
# Run Tests
|
||||
echo "Running integration tests..."
|
||||
cd tests/integration
|
||||
npm test
|
||||
31
tests/integration/setup.ts
Normal file
31
tests/integration/setup.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||
|
||||
const SUPABASE_URL = process.env.MADBASE_URL || 'http://localhost:8000';
|
||||
const SUPABASE_ANON_KEY = process.env.MADBASE_ANON_KEY || '';
|
||||
const SUPABASE_SERVICE_ROLE_KEY = process.env.MADBASE_SERVICE_ROLE_KEY || '';
|
||||
|
||||
if (!SUPABASE_ANON_KEY || !SUPABASE_SERVICE_ROLE_KEY) {
|
||||
throw new Error('Missing MADBASE_ANON_KEY or MADBASE_SERVICE_ROLE_KEY');
|
||||
}
|
||||
|
||||
export const createAnonClient = (): SupabaseClient => {
|
||||
return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const createServiceRoleClient = (): SupabaseClient => {
|
||||
return createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
53
tests/integration/setup_db.sql
Normal file
53
tests/integration/setup_db.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
DROP TABLE IF EXISTS public.todos;
|
||||
CREATE TABLE public.todos (
|
||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
title text NOT NULL,
|
||||
completed boolean DEFAULT false,
|
||||
user_id uuid, -- For RLS testing later
|
||||
created_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Grants for public
|
||||
GRANT ALL ON public.todos TO anon, authenticated;
|
||||
|
||||
-- Grants for Realtime schema
|
||||
GRANT USAGE ON SCHEMA madbase_realtime TO anon, authenticated;
|
||||
GRANT ALL ON ALL TABLES IN SCHEMA madbase_realtime TO anon, authenticated;
|
||||
GRANT ALL ON ALL SEQUENCES IN SCHEMA madbase_realtime TO anon, authenticated;
|
||||
GRANT ALL ON ALL FUNCTIONS IN SCHEMA madbase_realtime TO anon, authenticated;
|
||||
|
||||
-- Allow everything for anon for now to test basic CRUD
|
||||
CREATE POLICY "Allow anon select" ON public.todos FOR SELECT TO anon USING (true);
|
||||
CREATE POLICY "Allow anon insert" ON public.todos FOR INSERT TO anon WITH CHECK (true);
|
||||
CREATE POLICY "Allow anon update" ON public.todos FOR UPDATE TO anon USING (true);
|
||||
CREATE POLICY "Allow anon delete" ON public.todos FOR DELETE TO anon USING (true);
|
||||
|
||||
-- Allow authenticated users
|
||||
CREATE POLICY "Allow auth select" ON public.todos FOR SELECT TO authenticated USING (true);
|
||||
CREATE POLICY "Allow auth insert" ON public.todos FOR INSERT TO authenticated WITH CHECK (true);
|
||||
CREATE POLICY "Allow auth update" ON public.todos FOR UPDATE TO authenticated USING (true);
|
||||
CREATE POLICY "Allow auth delete" ON public.todos FOR DELETE TO authenticated USING (true);
|
||||
|
||||
-- Enable Realtime
|
||||
CREATE TRIGGER realtime_todos
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.todos
|
||||
FOR EACH ROW EXECUTE FUNCTION madbase_realtime.broadcast_changes();
|
||||
|
||||
-- Storage Setup
|
||||
INSERT INTO storage.buckets (id, name, public) VALUES ('test-bucket', 'test-bucket', true) ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Allow anon to upload to test-bucket
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_policies WHERE tablename = 'objects' AND policyname = 'Anon can insert into test-bucket'
|
||||
) THEN
|
||||
CREATE POLICY "Anon can insert into test-bucket"
|
||||
ON storage.objects FOR INSERT
|
||||
TO anon
|
||||
WITH CHECK ( bucket_id = 'test-bucket' );
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
39
tests/integration/storage.test.ts
Normal file
39
tests/integration/storage.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createAnonClient, createServiceRoleClient } from './setup.ts';
|
||||
|
||||
const client = createAnonClient();
|
||||
const admin = createServiceRoleClient();
|
||||
const bucket = 'test-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 });
|
||||
|
||||
if (error) console.error('Upload error:', error);
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeDefined();
|
||||
expect(data?.path).toBe('hello.txt');
|
||||
});
|
||||
|
||||
it('should list files', async () => {
|
||||
const { data, error } = await client.storage.from(bucket).list();
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeDefined();
|
||||
expect(data?.some((f) => f.name === 'hello.txt')).toBe(true);
|
||||
});
|
||||
|
||||
it('should download a file', async () => {
|
||||
const { data, error } = await client.storage.from(bucket).download('hello.txt');
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(data).toBeDefined();
|
||||
const text = await data?.text();
|
||||
expect(text).toBe('Hello, MadBase!');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user