Refactors the POST handler into handleLocalFriendRequest + handleFederatedFriendRequest helpers. The federated branch resolves the target domain, ensures peering, looks up the remote user, creates/hydrates a replicated stub, and writes a transactional (friend_requests + mutation_log + outbox) event with relayMessageId set. Also exports hydrateReplicatedUserProfile from federation.ts and adds the T11 happy-path test.
174 lines
6.1 KiB
TypeScript
174 lines
6.1 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
import Fastify, { type FastifyInstance } from 'fastify';
|
|
import Database from 'better-sqlite3';
|
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
import { eq } from 'drizzle-orm';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import * as schema from '../db/schema.js';
|
|
import { setWorkerId } from '../utils/snowflake.js';
|
|
|
|
setWorkerId(1);
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const CALLER_ID = 'caller-id';
|
|
|
|
let sqlite: Database.Database;
|
|
let testDb: ReturnType<typeof drizzle<typeof schema>>;
|
|
const sendToUser = vi.fn();
|
|
const ensurePeeredMock = vi.fn();
|
|
const lookupRemoteUserMock = vi.fn();
|
|
const resolveOriginFromHostnameMock = vi.fn();
|
|
|
|
vi.mock('../db/index.js', () => ({ getDb: () => testDb, getRawDb: () => sqlite, schema }));
|
|
vi.mock('../utils/auth.js', () => ({
|
|
authenticate: async (req: { userId?: string }) => { req.userId = CALLER_ID; },
|
|
}));
|
|
vi.mock('../ws/handler.js', () => ({
|
|
connectionManager: { sendToUser, sendToAdmins: vi.fn(), sendToDmMembers: vi.fn(), getAllOnlineUserIds: () => [] },
|
|
}));
|
|
vi.mock('../utils/federationAuth.js', async (importActual) => {
|
|
const actual = await importActual<typeof import('../utils/federationAuth.js')>();
|
|
return { ...actual, getOurOrigin: () => 'https://home.test' };
|
|
});
|
|
vi.mock('../utils/federationPeering.js', () => ({
|
|
ensurePeered: (...args: unknown[]) => ensurePeeredMock(...args),
|
|
racePeering: vi.fn(),
|
|
}));
|
|
vi.mock('../utils/federationLookup.js', () => ({
|
|
lookupRemoteUser: (...args: unknown[]) => lookupRemoteUserMock(...args),
|
|
}));
|
|
vi.mock('../utils/federationOriginResolve.js', () => ({
|
|
resolveOriginFromHostname: (...args: unknown[]) => resolveOriginFromHostnameMock(...args),
|
|
}));
|
|
|
|
function applyMigrations(db: Database.Database): void {
|
|
const dir = path.resolve(__dirname, '../../drizzle');
|
|
for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.sql')).sort()) {
|
|
const sqlText = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
for (const stmt of sqlText.split(/-->\s*statement-breakpoint/)) {
|
|
const clean = stmt.trim();
|
|
if (clean) db.exec(clean);
|
|
}
|
|
}
|
|
}
|
|
|
|
function seedSelf(opts: { homeInstance?: string | null; homeUserId?: string | null } = {}): void {
|
|
testDb.insert(schema.users).values({
|
|
id: CALLER_ID,
|
|
username: 'caller',
|
|
displayName: 'Caller',
|
|
passwordHash: 'x',
|
|
status: 'online',
|
|
isAdmin: 0,
|
|
homeInstance: opts.homeInstance ?? null,
|
|
homeUserId: opts.homeUserId ?? null,
|
|
createdAt: Date.now(),
|
|
}).run();
|
|
// Seed instance_settings with relay enabled so queue/log writes are not silently skipped.
|
|
// Use raw exec to avoid the updatedAt NOT NULL constraint (no default in schema).
|
|
sqlite.exec(`INSERT OR IGNORE INTO instance_settings (id, federation_relay_enabled, updated_at) VALUES (1, 1, ${Date.now()})`);
|
|
}
|
|
|
|
async function buildApp(): Promise<FastifyInstance> {
|
|
const app = Fastify({ logger: false });
|
|
const { socialRoutes } = await import('./social.js');
|
|
await app.register(socialRoutes);
|
|
await app.ready();
|
|
return app;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
sqlite = new Database(':memory:');
|
|
testDb = drizzle(sqlite, { schema });
|
|
applyMigrations(sqlite);
|
|
sendToUser.mockReset();
|
|
ensurePeeredMock.mockReset();
|
|
lookupRemoteUserMock.mockReset();
|
|
resolveOriginFromHostnameMock.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('POST /api/social/requests — federated branch (happy path)', () => {
|
|
it('creates a stub, inserts the request with relayMessageId, queues a relay event', async () => {
|
|
seedSelf();
|
|
|
|
resolveOriginFromHostnameMock.mockReturnValue('https://orbit.test');
|
|
ensurePeeredMock.mockResolvedValue({ status: 'active', peerId: 'peer-1' });
|
|
lookupRemoteUserMock.mockResolvedValue({
|
|
ok: true,
|
|
homeUserId: 'remote-alice',
|
|
username: 'alice',
|
|
profile: {
|
|
displayName: 'Alice',
|
|
avatar: null,
|
|
avatarColor: 'mint',
|
|
banner: null,
|
|
bio: 'hi',
|
|
},
|
|
});
|
|
|
|
const app = await buildApp();
|
|
const res = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/social/requests',
|
|
payload: { username: 'alice@orbit.test' },
|
|
});
|
|
|
|
// Status and body
|
|
expect(res.statusCode).toBe(201);
|
|
const body = JSON.parse(res.body) as { success: boolean; requestId: string };
|
|
expect(body.success).toBe(true);
|
|
expect(typeof body.requestId).toBe('string');
|
|
|
|
// friend_requests row
|
|
const reqRow = testDb.select().from(schema.friendRequests)
|
|
.where(eq(schema.friendRequests.id, body.requestId))
|
|
.get();
|
|
expect(reqRow).toBeTruthy();
|
|
expect(reqRow!.fromId).toBe(CALLER_ID);
|
|
expect(reqRow!.relayMessageId).toMatch(/^friend_req:/);
|
|
|
|
// Stub user exists with correct federated identity
|
|
const stub = testDb.select().from(schema.users)
|
|
.where(eq(schema.users.homeUserId, 'remote-alice'))
|
|
.get();
|
|
expect(stub).toBeTruthy();
|
|
expect(stub!.homeInstance).toBe('orbit.test');
|
|
expect(stub!.displayName).toBe('Alice');
|
|
expect(stub!.bio).toBe('hi');
|
|
|
|
// federation_outbox row
|
|
const peer = testDb.select({ id: schema.federationPeers.id, origin: schema.federationPeers.origin })
|
|
.from(schema.federationPeers)
|
|
.where(eq(schema.federationPeers.origin, 'https://orbit.test'))
|
|
.get();
|
|
expect(peer).toBeTruthy();
|
|
|
|
const outboxRow = testDb.select().from(schema.federationOutbox)
|
|
.where(eq(schema.federationOutbox.peerId, peer!.id))
|
|
.get();
|
|
expect(outboxRow).toBeTruthy();
|
|
expect(outboxRow!.eventType).toBe('friend_request_create');
|
|
expect(outboxRow!.entityId).toBe(reqRow!.relayMessageId);
|
|
|
|
// federation_mutation_log row
|
|
const mutationRow = testDb.select().from(schema.federationMutationLog)
|
|
.where(eq(schema.federationMutationLog.entityId, reqRow!.relayMessageId!))
|
|
.get();
|
|
expect(mutationRow).toBeTruthy();
|
|
|
|
// WS broadcast to sender
|
|
expect(sendToUser).toHaveBeenCalledWith(
|
|
CALLER_ID,
|
|
expect.objectContaining({
|
|
type: 'friend_request_sent',
|
|
request: expect.objectContaining({ id: body.requestId }),
|
|
}),
|
|
);
|
|
});
|
|
});
|