Files
backspace/packages/server/src/routes/federation.resolveOrCreate.test.ts
T

152 lines
5.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as schema from '../db/schema.js';
import { eq } from 'drizzle-orm';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
type TestDb = ReturnType<typeof drizzle<typeof schema>>;
let sqlite: Database.Database;
let testDb: TestDb;
vi.mock('../db/index.js', () => ({
getDb: () => testDb,
getRawDb: () => sqlite,
schema,
}));
let _sf = 1;
vi.mock('../utils/snowflake.js', () => ({
generateSnowflake: () => String(_sf++),
setWorkerId: vi.fn(),
}));
vi.mock('../utils/federationAuth.js', async (importActual) => {
const actual = await importActual<typeof import('../utils/federationAuth.js')>();
return { ...actual, getOurOrigin: () => 'https://home.test' };
});
// federation.ts also imports connectionManager/ws — stub minimal surface so
// the route module loads at test time. The function under test doesn't touch any of these.
vi.mock('../ws/handler.js', () => ({
connectionManager: {
sendToUser: vi.fn(),
sendToSpace: vi.fn(),
sendToDmMembers: vi.fn(),
sendToAdmins: vi.fn(),
getAllOnlineUserIds: () => [],
evictFederatedCallsForHost: vi.fn(),
federatedCalls: new Map(),
isUserOnline: vi.fn(),
lateBindFederatedCall: vi.fn(),
},
}));
function applyMigrations(db: Database.Database): void {
const migrationsDir = path.resolve(__dirname, '../../drizzle');
const files = fs.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
for (const f of files) {
const sql = fs.readFileSync(path.join(migrationsDir, f), 'utf8');
for (const stmt of sql.split(/-->\s*statement-breakpoint/)) {
const clean = stmt.trim();
if (clean) db.exec(clean);
}
}
}
beforeEach(() => {
sqlite = new Database(':memory:');
testDb = drizzle(sqlite, { schema });
applyMigrations(sqlite);
_sf = 1;
});
describe('resolveOrCreateReplicatedUser — stub username', () => {
it('creates stub with realname@domain when hint provides username', async () => {
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
const created = resolveOrCreateReplicatedUser(
'310002371434024960',
'orbit.ddns.net',
testDb,
{ username: 'pbtest3' },
);
expect(created).not.toBeNull();
expect(created!.username).toBe('pbtest3@orbit.ddns.net');
expect(created!.homeUserId).toBe('310002371434024960');
expect(created!.homeInstance).toBe('orbit.ddns.net');
});
it('falls back to homeUserId@domain when no hint provided', async () => {
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
const created = resolveOrCreateReplicatedUser(
'310002371434024960',
'orbit.ddns.net',
testDb,
);
expect(created!.username).toBe('310002371434024960@orbit.ddns.net');
});
});
describe('resolveOrCreateReplicatedUser — self-homed identity guard', () => {
it('refuses to create a stub homed at our own domain (dead incarnation)', async () => {
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
const result = resolveOrCreateReplicatedUser(
'dead-incarnation-id',
'home.test',
testDb,
{ username: 'youruser' },
);
expect(result).toBeNull();
const rows = testDb.select().from(schema.users).all();
expect(rows).toHaveLength(0);
});
it('refuses self-homed creation regardless of homeInstance URL shape', async () => {
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
expect(resolveOrCreateReplicatedUser('dead-1', 'https://home.test', testDb, { username: 'x' })).toBeNull();
expect(resolveOrCreateReplicatedUser('dead-2', 'HOME.TEST', testDb, { username: 'x' })).toBeNull();
expect(testDb.select().from(schema.users).all()).toHaveLength(0);
});
it('still resolves a LIVE native user referenced by self-domain identity (tier 1)', async () => {
testDb.insert(schema.users).values({
id: 'native-1',
username: 'alice',
passwordHash: 'real-hash',
homeInstance: null,
createdAt: 1,
}).run();
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
const result = resolveOrCreateReplicatedUser('native-1', 'https://home.test', testDb, { username: 'alice' });
expect(result).not.toBeNull();
expect(result!.id).toBe('native-1');
});
it('still creates stubs for remote-domain identities (unchanged behavior)', async () => {
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
const result = resolveOrCreateReplicatedUser('remote-1', 'orbit.ddns.net', testDb, { username: 'bob' });
expect(result).not.toBeNull();
expect(result!.username).toBe('bob@orbit.ddns.net');
});
it('refuses stub creation when the wire snapshot marks the identity deleted', async () => {
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
const result = resolveOrCreateReplicatedUser('remote-del', 'orbit.ddns.net', testDb, { username: null, deleted: true });
expect(result).toBeNull();
expect(testDb.select().from(schema.users).all()).toHaveLength(0);
});
it('a deleted-marked identity that already resolves locally still returns the existing row', async () => {
testDb.insert(schema.users).values({
id: 'stub-1', username: 'old@orbit.ddns.net', passwordHash: '!federation-replicated',
homeInstance: 'orbit.ddns.net', homeUserId: 'remote-del', createdAt: 1,
}).run();
const { resolveOrCreateReplicatedUser } = await import('./federation.js');
const result = resolveOrCreateReplicatedUser('remote-del', 'orbit.ddns.net', testDb, { deleted: true });
expect(result?.id).toBe('stub-1'); // historical attribution stays intact
});
});