mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
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.
123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
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);
|
|
});
|
|
});
|