chore: upgrade Node.js runtime to v24 across codebase #419
@@ -10,7 +10,7 @@ import { ConnectionList } from "@/components/federation/ConnectionList";
|
||||
import { InitiateConnectionDialog } from "@/components/federation/InitiateConnectionDialog";
|
||||
import { ComingSoon } from "@/components/ui/ComingSoon";
|
||||
import {
|
||||
mockConnections,
|
||||
getMockConnections,
|
||||
FederationConnectionStatus,
|
||||
type ConnectionDetails,
|
||||
} from "@/lib/api/federation";
|
||||
@@ -54,7 +54,7 @@ function ConnectionsPageContent(): React.JSX.Element {
|
||||
|
||||
// Using mock data for now (development only)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate network delay
|
||||
setConnections(mockConnections);
|
||||
setConnections(getMockConnections());
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : "Unable to load connections. Please try again."
|
||||
|
||||
194
apps/web/src/lib/api/federation.test.ts
Normal file
194
apps/web/src/lib/api/federation.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Federation API Client Tests
|
||||
* Tests for mock data NODE_ENV gating (SEC-WEB-37)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as client from "./client";
|
||||
import {
|
||||
getMockConnections,
|
||||
fetchConnections,
|
||||
fetchConnection,
|
||||
fetchInstanceIdentity,
|
||||
updateInstanceConfiguration,
|
||||
regenerateInstanceKeys,
|
||||
FederationConnectionStatus,
|
||||
} from "./federation";
|
||||
|
||||
// Mock the API client
|
||||
vi.mock("./client", () => ({
|
||||
apiGet: vi.fn(),
|
||||
apiPost: vi.fn(),
|
||||
apiPatch: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Federation API", () => {
|
||||
describe("getMockConnections", () => {
|
||||
it("should return mock connections in development mode", () => {
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
|
||||
const connections = getMockConnections();
|
||||
|
||||
expect(connections).toHaveLength(3);
|
||||
expect(connections[0]?.id).toBe("conn-1");
|
||||
expect(connections[0]?.remoteUrl).toBe("https://mosaic.work.example.com");
|
||||
expect(connections[1]?.id).toBe("conn-2");
|
||||
expect(connections[2]?.id).toBe("conn-3");
|
||||
});
|
||||
|
||||
it("should return empty array in production mode", () => {
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
|
||||
const connections = getMockConnections();
|
||||
|
||||
expect(connections).toEqual([]);
|
||||
expect(connections).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return empty array in test mode", () => {
|
||||
vi.stubEnv("NODE_ENV", "test");
|
||||
|
||||
const connections = getMockConnections();
|
||||
|
||||
expect(connections).toEqual([]);
|
||||
expect(connections).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should include expected connection statuses in development", () => {
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
|
||||
const connections = getMockConnections();
|
||||
|
||||
expect(connections[0]?.status).toBe(FederationConnectionStatus.ACTIVE);
|
||||
expect(connections[1]?.status).toBe(FederationConnectionStatus.PENDING);
|
||||
expect(connections[2]?.status).toBe(FederationConnectionStatus.DISCONNECTED);
|
||||
});
|
||||
|
||||
it("should include capabilities in development mock data", () => {
|
||||
vi.stubEnv("NODE_ENV", "development");
|
||||
|
||||
const connections = getMockConnections();
|
||||
|
||||
expect(connections[0]?.remoteCapabilities).toEqual({
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: true,
|
||||
protocolVersion: "1.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not expose mock public keys in production", () => {
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
|
||||
const connections = getMockConnections();
|
||||
|
||||
// In production, no connections should be returned at all
|
||||
expect(connections).toHaveLength(0);
|
||||
// Verify no public key data is accessible
|
||||
const hasPublicKeys = connections.some((c) => c.remotePublicKey);
|
||||
expect(hasPublicKeys).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchConnections", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should call the connections endpoint without filters", async () => {
|
||||
const mockResponse = [{ id: "conn-1" }];
|
||||
|
||||
vi.mocked(client.apiGet).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await fetchConnections();
|
||||
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/v1/federation/connections");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it("should include status filter in query string", async () => {
|
||||
const mockResponse = [{ id: "conn-1" }];
|
||||
|
||||
vi.mocked(client.apiGet).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await fetchConnections(FederationConnectionStatus.ACTIVE);
|
||||
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/v1/federation/connections?status=ACTIVE");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchConnection", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch a single connection by ID", async () => {
|
||||
const mockResponse = { id: "conn-1", remoteUrl: "https://example.com" };
|
||||
|
||||
vi.mocked(client.apiGet).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await fetchConnection("conn-1");
|
||||
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/v1/federation/connections/conn-1");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchInstanceIdentity", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch the instance identity", async () => {
|
||||
const mockResponse = { id: "inst-1", name: "Test Instance" };
|
||||
|
||||
vi.mocked(client.apiGet).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await fetchInstanceIdentity();
|
||||
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/v1/federation/instance");
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateInstanceConfiguration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should update instance configuration", async () => {
|
||||
const mockResponse = { id: "inst-1", name: "Updated Instance" };
|
||||
|
||||
vi.mocked(client.apiPatch).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateInstanceConfiguration({ name: "Updated Instance" });
|
||||
|
||||
expect(client.apiPatch).toHaveBeenCalledWith("/api/v1/federation/instance", {
|
||||
name: "Updated Instance",
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe("regenerateInstanceKeys", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should regenerate instance keys", async () => {
|
||||
const mockResponse = { id: "inst-1", publicKey: "new-key" };
|
||||
|
||||
vi.mocked(client.apiPost).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await regenerateInstanceKeys();
|
||||
|
||||
expect(client.apiPost).toHaveBeenCalledWith(
|
||||
"/api/v1/federation/instance/regenerate-keys",
|
||||
{}
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -197,76 +197,85 @@ export async function regenerateInstanceKeys(): Promise<PublicInstanceIdentity>
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock connections for development
|
||||
* Get mock connections for development only.
|
||||
* Returns an empty array in production as defense-in-depth.
|
||||
* The federation pages are also gated behind a ComingSoon component
|
||||
* in production (SEC-WEB-4), but this provides an additional layer.
|
||||
*/
|
||||
export const mockConnections: ConnectionDetails[] = [
|
||||
{
|
||||
id: "conn-1",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-work-001",
|
||||
remoteUrl: "https://mosaic.work.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: true,
|
||||
protocolVersion: "1.0",
|
||||
export function getMockConnections(): ConnectionDetails[] {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: "conn-1",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-work-001",
|
||||
remoteUrl: "https://mosaic.work.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: true,
|
||||
protocolVersion: "1.0",
|
||||
},
|
||||
status: FederationConnectionStatus.ACTIVE,
|
||||
metadata: {
|
||||
name: "Work Instance",
|
||||
description: "Corporate Mosaic instance",
|
||||
},
|
||||
createdAt: new Date("2026-02-01").toISOString(),
|
||||
updatedAt: new Date("2026-02-01").toISOString(),
|
||||
connectedAt: new Date("2026-02-01").toISOString(),
|
||||
disconnectedAt: null,
|
||||
},
|
||||
status: FederationConnectionStatus.ACTIVE,
|
||||
metadata: {
|
||||
name: "Work Instance",
|
||||
description: "Corporate Mosaic instance",
|
||||
{
|
||||
id: "conn-2",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-partner-001",
|
||||
remoteUrl: "https://mosaic.partner.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: false,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: false,
|
||||
protocolVersion: "1.0",
|
||||
},
|
||||
status: FederationConnectionStatus.PENDING,
|
||||
metadata: {
|
||||
name: "Partner Instance",
|
||||
description: "Shared project collaboration",
|
||||
},
|
||||
createdAt: new Date("2026-02-02").toISOString(),
|
||||
updatedAt: new Date("2026-02-02").toISOString(),
|
||||
connectedAt: null,
|
||||
disconnectedAt: null,
|
||||
},
|
||||
createdAt: new Date("2026-02-01").toISOString(),
|
||||
updatedAt: new Date("2026-02-01").toISOString(),
|
||||
connectedAt: new Date("2026-02-01").toISOString(),
|
||||
disconnectedAt: null,
|
||||
},
|
||||
{
|
||||
id: "conn-2",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-partner-001",
|
||||
remoteUrl: "https://mosaic.partner.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: false,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: false,
|
||||
protocolVersion: "1.0",
|
||||
{
|
||||
id: "conn-3",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-old-001",
|
||||
remoteUrl: "https://mosaic.old.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: false,
|
||||
supportsAgentSpawn: false,
|
||||
protocolVersion: "1.0",
|
||||
},
|
||||
status: FederationConnectionStatus.DISCONNECTED,
|
||||
metadata: {
|
||||
name: "Previous Instance",
|
||||
description: "No longer in use",
|
||||
},
|
||||
createdAt: new Date("2026-01-15").toISOString(),
|
||||
updatedAt: new Date("2026-01-30").toISOString(),
|
||||
connectedAt: new Date("2026-01-15").toISOString(),
|
||||
disconnectedAt: new Date("2026-01-30").toISOString(),
|
||||
},
|
||||
status: FederationConnectionStatus.PENDING,
|
||||
metadata: {
|
||||
name: "Partner Instance",
|
||||
description: "Shared project collaboration",
|
||||
},
|
||||
createdAt: new Date("2026-02-02").toISOString(),
|
||||
updatedAt: new Date("2026-02-02").toISOString(),
|
||||
connectedAt: null,
|
||||
disconnectedAt: null,
|
||||
},
|
||||
{
|
||||
id: "conn-3",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-old-001",
|
||||
remoteUrl: "https://mosaic.old.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: false,
|
||||
supportsAgentSpawn: false,
|
||||
protocolVersion: "1.0",
|
||||
},
|
||||
status: FederationConnectionStatus.DISCONNECTED,
|
||||
metadata: {
|
||||
name: "Previous Instance",
|
||||
description: "No longer in use",
|
||||
},
|
||||
createdAt: new Date("2026-01-15").toISOString(),
|
||||
updatedAt: new Date("2026-01-30").toISOString(),
|
||||
connectedAt: new Date("2026-01-15").toISOString(),
|
||||
disconnectedAt: new Date("2026-01-30").toISOString(),
|
||||
},
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user