fix(federation): persist remote instance_name from approval-requests/:id/approve handshake response
Third initiator path that calls remote /peer/accept. Mirrors performHandshake (auto-peer) and /peer/initiate (admin-initiate) — same try/catch parse, same null-or-non-empty-string guard. Caught in final review of #33; same root cause as Bug #1, bundled rather than fragmented to a new backlog item.
This commit is contained in:
@@ -355,3 +355,77 @@ describe('POST /api/federation/peer/initiate — persists remote instanceName fr
|
|||||||
expect(row?.instanceName).toBeNull();
|
expect(row?.instanceName).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /api/federation/approval-requests/:id/approve — persists remote instanceName from handshake response', () => {
|
||||||
|
let app: FastifyInstance;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
sqlite = new Database(':memory:');
|
||||||
|
testDb = drizzle(sqlite, { schema });
|
||||||
|
applyMigrations(sqlite);
|
||||||
|
seedInstanceSettings('Local Backspace');
|
||||||
|
app = await buildApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
function seedApprovalRequest(): string {
|
||||||
|
const id = 'approval-1';
|
||||||
|
testDb.insert(schema.peerApprovalRequests).values({
|
||||||
|
id,
|
||||||
|
origin: 'https://remote.example',
|
||||||
|
instanceName: 'Stale Name',
|
||||||
|
hmacSecret: 'their-old-secret',
|
||||||
|
requestedAt: Date.now(),
|
||||||
|
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||||
|
}).run();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('writes remote.instanceName when remote /peer/accept succeeds', async () => {
|
||||||
|
const approvalId = seedApprovalRequest();
|
||||||
|
|
||||||
|
vi.stubGlobal('fetch', vi.fn(async () =>
|
||||||
|
new Response(JSON.stringify({ accepted: true, instanceName: 'Remote Backspace' }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/federation/approval-requests/${approvalId}/approve`,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
const row = testDb.select().from(schema.federationPeers)
|
||||||
|
.where(eq(schema.federationPeers.origin, 'https://remote.example')).get();
|
||||||
|
expect(row?.status).toBe('active');
|
||||||
|
expect(row?.instanceName).toBe('Remote Backspace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes null instanceName when remote response omits the field', async () => {
|
||||||
|
const approvalId = seedApprovalRequest();
|
||||||
|
|
||||||
|
vi.stubGlobal('fetch', vi.fn(async () =>
|
||||||
|
new Response(JSON.stringify({ accepted: true }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/federation/approval-requests/${approvalId}/approve`,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
const row = testDb.select().from(schema.federationPeers)
|
||||||
|
.where(eq(schema.federationPeers.origin, 'https://remote.example')).get();
|
||||||
|
expect(row?.instanceName).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1169,8 +1169,21 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
return reply.code(502).send({ error: errorMessage, statusCode: 502 });
|
return reply.code(502).send({ error: errorMessage, statusCode: 502 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse the remote's instanceName from the response body so the
|
||||||
|
// federation panel renders a friendly label. Tolerate omission and
|
||||||
|
// non-JSON bodies — same pattern as performHandshake and /peer/initiate.
|
||||||
|
let remoteInstanceName: string | null = null;
|
||||||
|
try {
|
||||||
|
const body = (await response.json()) as { instanceName?: string | null };
|
||||||
|
if (typeof body?.instanceName === 'string' && body.instanceName.length > 0) {
|
||||||
|
remoteInstanceName = body.instanceName;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-JSON body — leave null.
|
||||||
|
}
|
||||||
|
|
||||||
db.update(schema.federationPeers)
|
db.update(schema.federationPeers)
|
||||||
.set({ status: 'active', lastSeenAt: now })
|
.set({ status: 'active', lastSeenAt: now, instanceName: remoteInstanceName })
|
||||||
.where(eq(schema.federationPeers.id, peerId))
|
.where(eq(schema.federationPeers.id, peerId))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user