test(federation-identity): #1 leave mode — registry cleaned, remote untouched, no S2S call
Also fix dbInspect.ts UserRow column aliases: SELECT * returns snake_case columns (is_deleted, display_name, etc.) but UserRow expected camelCase. Switch to explicit aliased SELECT so all callers get the documented interface.
This commit is contained in:
@@ -46,4 +46,51 @@ describe('Federation identity deletion — server suite', () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(String(body.error)).toMatch(/origins/);
|
expect(String(body.error)).toMatch(/origins/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#1 leave mode: home registry cleaned, remote untouched, no S2S request hits the remote', async () => {
|
||||||
|
const { createFederatedUser } = await import('./helpers/testUsers.js');
|
||||||
|
const { openInspector } = await import('./helpers/dbInspect.js');
|
||||||
|
const { logMatched } = await import('./helpers/twoInstanceHarness.js');
|
||||||
|
const { homeUser, remoteUser } = await createFederatedUser(harness.home, harness.remote, 't1');
|
||||||
|
|
||||||
|
// Snapshot remote user pre-delete
|
||||||
|
const remoteInspect = openInspector(harness.remote);
|
||||||
|
const before = remoteInspect.user(remoteUser.id);
|
||||||
|
expect(before).toBeTruthy();
|
||||||
|
expect(before!.isDeleted).toBe(0);
|
||||||
|
remoteInspect.close();
|
||||||
|
|
||||||
|
const res = await fetch(`${harness.home.origin}/api/users/@me/federation-identity/delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${homeUser.token}` },
|
||||||
|
body: JSON.stringify({ origins: [harness.remote.origin], mode: 'leave' }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.results[harness.remote.origin]).toEqual({ success: true });
|
||||||
|
|
||||||
|
// CRITICAL: prove no S2S call hit the remote. Fastify access logs include the
|
||||||
|
// request path; if `/api/federation/identity` appears at all in remote.log
|
||||||
|
// after the call, leave mode incorrectly fired an S2S request.
|
||||||
|
const sawS2S = await logMatched(harness.remote, /\/api\/federation\/identity/, 1_000);
|
||||||
|
expect(sawS2S).toBe(false);
|
||||||
|
|
||||||
|
// Belt-and-suspenders: remote user row UNCHANGED in every column
|
||||||
|
const remoteAfter = openInspector(harness.remote);
|
||||||
|
const after = remoteAfter.user(remoteUser.id);
|
||||||
|
expect(after).toMatchObject({
|
||||||
|
id: before!.id,
|
||||||
|
username: before!.username,
|
||||||
|
isDeleted: 0,
|
||||||
|
displayName: before!.displayName,
|
||||||
|
});
|
||||||
|
remoteAfter.close();
|
||||||
|
|
||||||
|
// Home registry row gone, replicatedInstances trimmed
|
||||||
|
const homeInspect = openInspector(harness.home);
|
||||||
|
expect(homeInspect.registryRow(homeUser.id, harness.remote.origin)).toBeUndefined();
|
||||||
|
const replInst = homeInspect.replicatedInstancesArray(homeUser.id);
|
||||||
|
expect(replInst.find(r => r.origin === harness.remote.origin)).toBeUndefined();
|
||||||
|
homeInspect.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,8 +38,38 @@ export function openInspector(instance: SpawnedInstance): DbInspector {
|
|||||||
const db = new Database(instance.dbPath, { readonly: true, fileMustExist: true });
|
const db = new Database(instance.dbPath, { readonly: true, fileMustExist: true });
|
||||||
db.pragma('journal_mode = WAL');
|
db.pragma('journal_mode = WAL');
|
||||||
return {
|
return {
|
||||||
user: (uid) => db.prepare('SELECT * FROM users WHERE id = ?').get(uid) as UserRow | null,
|
user: (uid) => db.prepare(`
|
||||||
userByUsername: (u) => db.prepare('SELECT * FROM users WHERE username = ?').get(u) as UserRow | null,
|
SELECT id, username,
|
||||||
|
password_hash AS passwordHash,
|
||||||
|
display_name AS displayName,
|
||||||
|
avatar, banner, bio,
|
||||||
|
custom_status AS customStatus,
|
||||||
|
accent_color AS accentColor,
|
||||||
|
avatar_color AS avatarColor,
|
||||||
|
replicated_instances AS replicatedInstances,
|
||||||
|
is_deleted AS isDeleted,
|
||||||
|
status,
|
||||||
|
is_admin AS isAdmin,
|
||||||
|
home_instance AS homeInstance,
|
||||||
|
home_user_id AS homeUserId
|
||||||
|
FROM users WHERE id = ?
|
||||||
|
`).get(uid) as UserRow | null,
|
||||||
|
userByUsername: (u) => db.prepare(`
|
||||||
|
SELECT id, username,
|
||||||
|
password_hash AS passwordHash,
|
||||||
|
display_name AS displayName,
|
||||||
|
avatar, banner, bio,
|
||||||
|
custom_status AS customStatus,
|
||||||
|
accent_color AS accentColor,
|
||||||
|
avatar_color AS avatarColor,
|
||||||
|
replicated_instances AS replicatedInstances,
|
||||||
|
is_deleted AS isDeleted,
|
||||||
|
status,
|
||||||
|
is_admin AS isAdmin,
|
||||||
|
home_instance AS homeInstance,
|
||||||
|
home_user_id AS homeUserId
|
||||||
|
FROM users WHERE username = ?
|
||||||
|
`).get(u) as UserRow | null,
|
||||||
spaceMembersForUser: (uid) =>
|
spaceMembersForUser: (uid) =>
|
||||||
db.prepare('SELECT space_id AS spaceId, user_id AS userId FROM space_members WHERE user_id = ?').all(uid) as { spaceId: string; userId: string }[],
|
db.prepare('SELECT space_id AS spaceId, user_id AS userId FROM space_members WHERE user_id = ?').all(uid) as { spaceId: string; userId: string }[],
|
||||||
reactionsForUser: (uid) =>
|
reactionsForUser: (uid) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user