fix(federation): create replicated-user stubs with realname@domain when hint provided
When friend_request_create / friend_add / DM relay carries a profile snapshot, the canonical-username hint is now used as the stub's local-part. Stubs created purely from S2S (no client-federation) now display the human-readable handle, not the homeUserId snowflake. Falls back to the snowflake-id scheme only when no hint is available.
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
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(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3181,9 +3181,13 @@ export function resolveOrCreateReplicatedUser(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the snowflake-style homeUserId as the local part; append the
|
// Use the home user's real username when the caller passes a hint (the wire
|
||||||
// domain so the username is globally unique and human-readable.
|
// profile snapshot from friend_request_create / friend_add / DM relay carries
|
||||||
const baseUsername = `${homeUserId}@${domain}`.toLowerCase();
|
// it). This makes the local stub's `username` human-readable, so client-side
|
||||||
|
// `parseFederatedUsername(username).baseName` returns the real handle. Falls
|
||||||
|
// back to the snowflake-id scheme when no hint is available (legacy paths).
|
||||||
|
const localPart = (hints?.username ?? homeUserId).toLowerCase();
|
||||||
|
const baseUsername = `${localPart}@${domain}`.toLowerCase();
|
||||||
|
|
||||||
// Guard against the (unlikely) case where this username already
|
// Guard against the (unlikely) case where this username already
|
||||||
// exists — e.g. a prior partial replication or manual creation.
|
// exists — e.g. a prior partial replication or manual creation.
|
||||||
@@ -3192,11 +3196,11 @@ export function resolveOrCreateReplicatedUser(
|
|||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
while (collision) {
|
while (collision) {
|
||||||
attempt++;
|
attempt++;
|
||||||
username = `${homeUserId}_${attempt}@${domain}`.toLowerCase();
|
username = `${localPart}_${attempt}@${domain}`.toLowerCase();
|
||||||
collision = db.select().from(schema.users).where(eq(schema.users.username, username)).get();
|
collision = db.select().from(schema.users).where(eq(schema.users.username, username)).get();
|
||||||
if (attempt > 10) {
|
if (attempt > 10) {
|
||||||
// Extremely unlikely; use a random suffix to break out
|
// Extremely unlikely; use a random suffix to break out
|
||||||
username = `${homeUserId}_${randomBytes(4).toString('hex')}@${domain}`.toLowerCase();
|
username = `${localPart}_${randomBytes(4).toString('hex')}@${domain}`.toLowerCase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user