added initial roadmap and implementation

This commit is contained in:
2026-03-11 22:23:16 +02:00
parent 39b97a6db5
commit c0792f2e1d
62 changed files with 12410 additions and 1 deletions

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

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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
View 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

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

View 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
$$;

View 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!');
});
});