mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat(backend): add standalone data plane
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
This commit is contained in:
1 parent
01bb8225ae
commit
006761d3b0
294 files changed
+16259
-6138
No files matched your search
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createApp } from '../src/app.js';
|
||||
import type { AuthIdentity, AuthVerifier } from '../src/auth.js';
|
||||
import type {
|
||||
ApiRepository,
|
||||
MobileCompatibleProfile,
|
||||
} from '../src/db/repository.js';
|
||||
|
||||
const alice: AuthIdentity = {
|
||||
id: '11111111-1111-4111-8111-111111111111',
|
||||
email: 'alice@sauci.test',
|
||||
name: 'Alice',
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
class MemoryRepository implements ApiRepository {
|
||||
profiles = new Map<string, MobileCompatibleProfile>();
|
||||
interests = new Set<string>();
|
||||
ready = vi.fn(async () => undefined);
|
||||
close = vi.fn(async () => undefined);
|
||||
|
||||
async upsertProfile(identity: AuthIdentity): Promise<MobileCompatibleProfile> {
|
||||
const existing = this.profiles.get(identity.id);
|
||||
if (existing) return existing;
|
||||
const now = new Date().toISOString();
|
||||
const profile: MobileCompatibleProfile = {
|
||||
id: identity.id,
|
||||
name: identity.name,
|
||||
email: identity.email,
|
||||
avatar_url: identity.avatarUrl,
|
||||
push_token: null,
|
||||
is_premium: false,
|
||||
couple_id: null,
|
||||
gender: null,
|
||||
show_explicit_content: true,
|
||||
max_intensity: 5,
|
||||
public_key_jwk: null,
|
||||
hide_nsfw: false,
|
||||
onboarding_completed: false,
|
||||
onboarding_version: 0,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
this.profiles.set(identity.id, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
async getFeatureInterest(userId: string, feature: string) {
|
||||
return { feature, interested: this.interests.has(`${userId}:${feature}`) };
|
||||
}
|
||||
|
||||
async putFeatureInterest(userId: string, feature: string) {
|
||||
this.interests.add(`${userId}:${feature}`);
|
||||
return { feature, interested: true };
|
||||
}
|
||||
|
||||
async deleteFeatureInterest(userId: string, feature: string) {
|
||||
this.interests.delete(`${userId}:${feature}`);
|
||||
return { feature, interested: false };
|
||||
}
|
||||
}
|
||||
|
||||
function verifier(identity: AuthIdentity = alice): AuthVerifier {
|
||||
return {
|
||||
verify: vi.fn(async (token: string) => {
|
||||
if (token !== 'valid') throw new Error('invalid');
|
||||
return identity;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('Sauci API', () => {
|
||||
it('reports liveness without touching the database and readiness with a database check', async () => {
|
||||
const repository = new MemoryRepository();
|
||||
const app = createApp({ auth: verifier(), repository });
|
||||
|
||||
expect(await (await app.request('/health/live')).json()).toEqual({ status: 'ok' });
|
||||
expect(repository.ready).not.toHaveBeenCalled();
|
||||
expect((await app.request('/health/ready')).status).toBe(200);
|
||||
expect(repository.ready).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('fails closed when the bearer token is missing or invalid', async () => {
|
||||
const app = createApp({ auth: verifier(), repository: new MemoryRepository() });
|
||||
expect((await app.request('/v1/me')).status).toBe(401);
|
||||
expect((await app.request('/v1/me', { headers: { authorization: 'Bearer invalid' } })).status).toBe(401);
|
||||
});
|
||||
|
||||
it('idempotently bootstraps the authenticated profile', async () => {
|
||||
const repository = new MemoryRepository();
|
||||
const app = createApp({ auth: verifier(), repository });
|
||||
const request = () => app.request('/v1/me', { headers: { authorization: 'Bearer valid' } });
|
||||
|
||||
const first = await (await request()).json() as { profile: MobileCompatibleProfile };
|
||||
const second = await (await request()).json() as { profile: MobileCompatibleProfile };
|
||||
expect(first.profile).toMatchObject({ id: alice.id, email: alice.email, name: 'Alice' });
|
||||
expect(second.profile).toEqual(first.profile);
|
||||
expect(repository.profiles.size).toBe(1);
|
||||
});
|
||||
|
||||
it('derives feature-interest ownership exclusively from the token', async () => {
|
||||
const repository = new MemoryRepository();
|
||||
const app = createApp({ auth: verifier(), repository });
|
||||
const headers = { authorization: 'Bearer valid' };
|
||||
|
||||
expect(await (await app.request('/v1/me/feature-interests/better-chat', { headers })).json())
|
||||
.toEqual({ feature: 'better-chat', interested: false });
|
||||
expect(await (await app.request('/v1/me/feature-interests/better-chat', { method: 'PUT', headers })).json())
|
||||
.toEqual({ feature: 'better-chat', interested: true });
|
||||
expect(repository.interests.has(`${alice.id}:better-chat`)).toBe(true);
|
||||
expect(await (await app.request('/v1/me/feature-interests/better-chat', { method: 'DELETE', headers })).json())
|
||||
.toEqual({ feature: 'better-chat', interested: false });
|
||||
});
|
||||
|
||||
it('rejects non-canonical feature names', async () => {
|
||||
const app = createApp({ auth: verifier(), repository: new MemoryRepository() });
|
||||
const response = await app.request('/v1/me/feature-interests/Not%20Safe', {
|
||||
headers: { authorization: 'Bearer valid' },
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user