feat(#84): implement instance identity model for federation

Implemented the foundation of federation architecture with instance
identity and connection management:

Database Schema:
- Added Instance model for instance identity with keypair generation
- Added FederationConnection model for workspace-scoped connections
- Added FederationConnectionStatus enum (PENDING, ACTIVE, SUSPENDED, DISCONNECTED)

Service Layer:
- FederationService with instance identity management
- RSA 2048-bit keypair generation for signing
- Public identity endpoint (excludes private key)
- Keypair regeneration capability

API Endpoints:
- GET /api/v1/federation/instance - Returns public instance identity
- POST /api/v1/federation/instance/regenerate-keys - Admin keypair regeneration

Tests:
- 11 tests passing (7 service, 4 controller)
- 100% statement coverage, 100% function coverage
- Follows TDD principles (Red-Green-Refactor)

Configuration:
- Added INSTANCE_NAME and INSTANCE_URL environment variables
- Integrated FederationModule into AppModule

Refs #84

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-03 10:58:50 -06:00
parent 6e63508f97
commit 7989c089ef
10 changed files with 853 additions and 22 deletions

View File

@@ -0,0 +1,114 @@
/**
* Federation Controller Tests
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { FederationController } from "./federation.controller";
import { FederationService } from "./federation.service";
import { AuthGuard } from "../auth/guards/auth.guard";
import { PublicInstanceIdentity, InstanceIdentity } from "./types/instance.types";
describe("FederationController", () => {
let controller: FederationController;
let service: FederationService;
const mockPublicIdentity: PublicInstanceIdentity = {
id: "123e4567-e89b-12d3-a456-426614174000",
instanceId: "test-instance-id",
name: "Test Instance",
url: "https://test.example.com",
publicKey: "-----BEGIN PUBLIC KEY-----\nMOCK\n-----END PUBLIC KEY-----",
capabilities: {
supportsQuery: true,
supportsCommand: true,
supportsEvent: true,
protocolVersion: "1.0",
},
metadata: {},
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-01T00:00:00Z"),
};
const mockInstanceIdentity: InstanceIdentity = {
...mockPublicIdentity,
privateKey: "-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----",
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [FederationController],
providers: [
{
provide: FederationService,
useValue: {
getPublicIdentity: vi.fn(),
regenerateKeypair: vi.fn(),
},
},
],
})
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<FederationController>(FederationController);
service = module.get<FederationService>(FederationService);
});
describe("GET /instance", () => {
it("should return public instance identity", async () => {
// Arrange
vi.spyOn(service, "getPublicIdentity").mockResolvedValue(mockPublicIdentity);
// Act
const result = await controller.getInstance();
// Assert
expect(result).toEqual(mockPublicIdentity);
expect(service.getPublicIdentity).toHaveBeenCalledTimes(1);
});
it("should not expose private key", async () => {
// Arrange
vi.spyOn(service, "getPublicIdentity").mockResolvedValue(mockPublicIdentity);
// Act
const result = await controller.getInstance();
// Assert
expect(result).not.toHaveProperty("privateKey");
});
it("should return consistent identity across multiple calls", async () => {
// Arrange
vi.spyOn(service, "getPublicIdentity").mockResolvedValue(mockPublicIdentity);
// Act
const result1 = await controller.getInstance();
const result2 = await controller.getInstance();
// Assert
expect(result1).toEqual(result2);
expect(result1.instanceId).toEqual(result2.instanceId);
});
});
describe("POST /instance/regenerate-keys", () => {
it("should regenerate keypair and return updated identity", async () => {
// Arrange
const updatedIdentity = {
...mockInstanceIdentity,
publicKey: "NEW_PUBLIC_KEY",
};
vi.spyOn(service, "regenerateKeypair").mockResolvedValue(updatedIdentity);
// Act
const result = await controller.regenerateKeys();
// Assert
expect(result).toEqual(updatedIdentity);
expect(service.regenerateKeypair).toHaveBeenCalledTimes(1);
});
});
});