feat(FN-4869): add regression tests for inbound sync empty apiKey handling
Adds regression test coverage for the inbound node-sync empty apiKey validation path in `routes-nodes-sync.test.ts`. Fusion-Task-Id: FN-4869
This commit is contained in:
committed by
gsxdsm
parent
9339473732
commit
6aee8923db
@@ -1225,6 +1225,144 @@ describe("Node settings sync routes", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("FN-4869: inbound sync rejects empty local apiKey (FN-4868 G-01)", () => {
|
||||||
|
const syncReceiveBody = {
|
||||||
|
sourceNodeId: "node-remote-001",
|
||||||
|
exportedAt: "2026-04-14T10:00:00.000Z",
|
||||||
|
};
|
||||||
|
const authReceiveBody = {
|
||||||
|
authMaterial: {
|
||||||
|
version: 1,
|
||||||
|
exportedAt: "2026-04-14T10:00:00.000Z",
|
||||||
|
checksum: "x",
|
||||||
|
payload: { providerAuth: {} },
|
||||||
|
},
|
||||||
|
sourceNodeId: "node-remote-001",
|
||||||
|
timestamp: "2026-04-14T10:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeCases = [
|
||||||
|
{
|
||||||
|
route: "/api/settings/sync-receive",
|
||||||
|
body: syncReceiveBody,
|
||||||
|
assertRejectedMutation: () => {
|
||||||
|
expect(mockApplyRemoteSettings).not.toHaveBeenCalled();
|
||||||
|
expect(mockApplyAuthMaterialSnapshot).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
assertAcceptedMutation: () => {
|
||||||
|
expect(mockApplyRemoteSettings).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockApplyAuthMaterialSnapshot).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/api/settings/auth-receive",
|
||||||
|
body: authReceiveBody,
|
||||||
|
assertRejectedMutation: () => {
|
||||||
|
expect(mockApplyAuthMaterialSnapshot).not.toHaveBeenCalled();
|
||||||
|
expect(mockApplyRemoteSettings).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
assertAcceptedMutation: () => {
|
||||||
|
expect(mockApplyAuthMaterialSnapshot).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockApplyRemoteSettings).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const localApiKeyStates = [
|
||||||
|
{ label: "empty string", localNode: createMockLocalNode({ apiKey: "" }) },
|
||||||
|
{ label: "undefined", localNode: createMockLocalNode({ apiKey: undefined }) },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const authHeaders = ["Bearer ", "Bearer anything-non-empty"] as const;
|
||||||
|
const skippedCases = routeCases.map((routeCase) => [routeCase, localApiKeyStates[0], "Bearer " as const] as const);
|
||||||
|
|
||||||
|
// FN-4871 tracks route hardening; these rows are intentionally skipped until the fix lands.
|
||||||
|
it.skip.each(skippedCases)(
|
||||||
|
"returns 401 for %s when local apiKey is %s and header is %s",
|
||||||
|
async (routeCase, apiKeyState, authHeader) => {
|
||||||
|
mockListNodes.mockResolvedValue([apiKeyState.localNode]);
|
||||||
|
|
||||||
|
const res = await request(
|
||||||
|
app,
|
||||||
|
"POST",
|
||||||
|
routeCase.route,
|
||||||
|
JSON.stringify(routeCase.body),
|
||||||
|
{ "content-type": "application/json", Authorization: authHeader },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
routeCase.assertRejectedMutation();
|
||||||
|
expect(mockAuthStorageSet).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const rejectionCases = routeCases.flatMap((routeCase) =>
|
||||||
|
localApiKeyStates.flatMap((apiKeyState) =>
|
||||||
|
authHeaders
|
||||||
|
.map((authHeader) => [routeCase, apiKeyState, authHeader] as const)
|
||||||
|
.filter(([, state, header]) => !(state.label === "empty string" && header === "Bearer ")),
|
||||||
|
));
|
||||||
|
|
||||||
|
it.each(rejectionCases)(
|
||||||
|
"returns 401 for %s when local apiKey is %s and header is %s",
|
||||||
|
async (routeCase, apiKeyState, authHeader) => {
|
||||||
|
mockListNodes.mockResolvedValue([apiKeyState.localNode]);
|
||||||
|
|
||||||
|
const res = await request(
|
||||||
|
app,
|
||||||
|
"POST",
|
||||||
|
routeCase.route,
|
||||||
|
JSON.stringify(routeCase.body),
|
||||||
|
{ "content-type": "application/json", Authorization: authHeader },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
routeCase.assertRejectedMutation();
|
||||||
|
expect(mockAuthStorageSet).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(routeCases.flatMap((routeCase) =>
|
||||||
|
localApiKeyStates.flatMap((apiKeyState) =>
|
||||||
|
["Bearer null", "Bearer undefined"].map((authHeader) => [routeCase, apiKeyState, authHeader] as const))))(
|
||||||
|
"returns 401 for %s when local apiKey is %s and literal header is %s",
|
||||||
|
async (routeCase, apiKeyState, authHeader) => {
|
||||||
|
mockListNodes.mockResolvedValue([apiKeyState.localNode]);
|
||||||
|
|
||||||
|
const res = await request(
|
||||||
|
app,
|
||||||
|
"POST",
|
||||||
|
routeCase.route,
|
||||||
|
JSON.stringify(routeCase.body),
|
||||||
|
{ "content-type": "application/json", Authorization: authHeader },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
routeCase.assertRejectedMutation();
|
||||||
|
expect(mockAuthStorageSet).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(routeCases)(
|
||||||
|
"positive control: returns 200 for %s when local apiKey matches header",
|
||||||
|
async (routeCase) => {
|
||||||
|
const localNode = createMockLocalNode({ apiKey: "local-api-key-456" });
|
||||||
|
mockListNodes.mockResolvedValue([localNode]);
|
||||||
|
|
||||||
|
const res = await request(
|
||||||
|
app,
|
||||||
|
"POST",
|
||||||
|
routeCase.route,
|
||||||
|
JSON.stringify(routeCase.body),
|
||||||
|
{ "content-type": "application/json", Authorization: "Bearer local-api-key-456" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
routeCase.assertAcceptedMutation();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// ── GET /api/settings/auth-export ────────────────────────────────────
|
// ── GET /api/settings/auth-export ────────────────────────────────────
|
||||||
|
|
||||||
describe("GET /api/settings/auth-export", () => {
|
describe("GET /api/settings/auth-export", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user