@synth-deploy/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,1245 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterAll } from "vitest";
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import type { FastifyInstance } from "fastify";
|
|
4
|
+
import {
|
|
5
|
+
DecisionDebrief,
|
|
6
|
+
PartitionStore,
|
|
7
|
+
EnvironmentStore,
|
|
8
|
+
ArtifactStore,
|
|
9
|
+
SettingsStore,
|
|
10
|
+
TelemetryStore,
|
|
11
|
+
} from "@synth-deploy/core";
|
|
12
|
+
import { SynthAgent, InMemoryDeploymentStore } from "../src/agent/synth-agent.js";
|
|
13
|
+
import { registerPartitionRoutes } from "../src/api/partitions.js";
|
|
14
|
+
import { registerEnvironmentRoutes } from "../src/api/environments.js";
|
|
15
|
+
import { registerSettingsRoutes } from "../src/api/settings.js";
|
|
16
|
+
import { registerDeploymentRoutes } from "../src/api/deployments.js";
|
|
17
|
+
import { registerArtifactRoutes } from "../src/api/artifacts.js";
|
|
18
|
+
import { registerHealthRoutes } from "../src/api/health.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Mock auth — inject a test user with all permissions on every request
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function addMockAuth(app: FastifyInstance) {
|
|
25
|
+
app.addHook("onRequest", async (request) => {
|
|
26
|
+
request.user = {
|
|
27
|
+
id: "test-user-id" as any,
|
|
28
|
+
email: "test@example.com",
|
|
29
|
+
name: "Test User",
|
|
30
|
+
permissions: [
|
|
31
|
+
"deployment.create", "deployment.approve", "deployment.reject", "deployment.view", "deployment.rollback",
|
|
32
|
+
"artifact.create", "artifact.update", "artifact.annotate", "artifact.delete", "artifact.view",
|
|
33
|
+
"environment.create", "environment.update", "environment.delete", "environment.view",
|
|
34
|
+
"partition.create", "partition.update", "partition.delete", "partition.view",
|
|
35
|
+
"envoy.register", "envoy.configure", "envoy.view",
|
|
36
|
+
"settings.manage", "users.manage", "roles.manage",
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Shared test server factory
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
interface TestContext {
|
|
47
|
+
app: FastifyInstance;
|
|
48
|
+
diary: DecisionDebrief;
|
|
49
|
+
partitions: PartitionStore;
|
|
50
|
+
environments: EnvironmentStore;
|
|
51
|
+
deployments: InMemoryDeploymentStore;
|
|
52
|
+
artifactStore: ArtifactStore;
|
|
53
|
+
settings: SettingsStore;
|
|
54
|
+
telemetry: TelemetryStore;
|
|
55
|
+
agent: SynthAgent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function createTestServer(): Promise<TestContext> {
|
|
59
|
+
const diary = new DecisionDebrief();
|
|
60
|
+
const partitions = new PartitionStore();
|
|
61
|
+
const environments = new EnvironmentStore();
|
|
62
|
+
const deployments = new InMemoryDeploymentStore();
|
|
63
|
+
const artifactStore = new ArtifactStore();
|
|
64
|
+
const settings = new SettingsStore();
|
|
65
|
+
const telemetry = new TelemetryStore();
|
|
66
|
+
// Do NOT pass settings as settingsReader to SynthAgent — with Envoy-only
|
|
67
|
+
// enforcement (#115), a settingsReader triggers Envoy delegation which needs
|
|
68
|
+
// a real Envoy. Tests use the local execution path (no settingsReader).
|
|
69
|
+
const agent = new SynthAgent(
|
|
70
|
+
diary, deployments, artifactStore, environments, partitions,
|
|
71
|
+
undefined, { healthCheckBackoffMs: 1, executionDelayMs: 1 },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const app = Fastify();
|
|
75
|
+
addMockAuth(app);
|
|
76
|
+
registerPartitionRoutes(app, partitions, deployments, diary, telemetry);
|
|
77
|
+
registerEnvironmentRoutes(app, environments, deployments, telemetry);
|
|
78
|
+
registerSettingsRoutes(app, settings, telemetry);
|
|
79
|
+
registerDeploymentRoutes(app, deployments, diary, partitions, environments, artifactStore, settings, telemetry);
|
|
80
|
+
registerArtifactRoutes(app, artifactStore, telemetry);
|
|
81
|
+
registerHealthRoutes(app);
|
|
82
|
+
|
|
83
|
+
await app.ready();
|
|
84
|
+
return { app, diary, partitions, environments, deployments, artifactStore, settings, telemetry, agent };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Helper: creates a deployment via the new artifact-based API.
|
|
89
|
+
*/
|
|
90
|
+
async function deployViaHttp(
|
|
91
|
+
server: FastifyInstance,
|
|
92
|
+
params: { artifactId: string; partitionId?: string; environmentId: string; version?: string },
|
|
93
|
+
) {
|
|
94
|
+
return server.inject({
|
|
95
|
+
method: "POST",
|
|
96
|
+
url: "/api/deployments",
|
|
97
|
+
payload: {
|
|
98
|
+
artifactId: params.artifactId,
|
|
99
|
+
environmentId: params.environmentId,
|
|
100
|
+
partitionId: params.partitionId,
|
|
101
|
+
version: params.version,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ===========================================================================
|
|
107
|
+
// Partition Routes
|
|
108
|
+
// ===========================================================================
|
|
109
|
+
|
|
110
|
+
describe("Partition Routes", () => {
|
|
111
|
+
let ctx: TestContext;
|
|
112
|
+
|
|
113
|
+
beforeEach(async () => {
|
|
114
|
+
ctx = await createTestServer();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterAll(async () => {
|
|
118
|
+
// Fastify instances are cleaned up per-test via beforeEach
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// --- POST /api/partitions ---
|
|
122
|
+
|
|
123
|
+
describe("POST /api/partitions", () => {
|
|
124
|
+
it("creates a partition and returns 201", async () => {
|
|
125
|
+
const res = await ctx.app.inject({
|
|
126
|
+
method: "POST",
|
|
127
|
+
url: "/api/partitions",
|
|
128
|
+
payload: { name: "Acme Corp", variables: { DB_HOST: "acme-db" } },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(res.statusCode).toBe(201);
|
|
132
|
+
const body = JSON.parse(res.payload);
|
|
133
|
+
expect(body.partition).toBeDefined();
|
|
134
|
+
expect(body.partition.name).toBe("Acme Corp");
|
|
135
|
+
expect(body.partition.variables.DB_HOST).toBe("acme-db");
|
|
136
|
+
expect(body.partition.id).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("creates a partition without variables", async () => {
|
|
140
|
+
const res = await ctx.app.inject({
|
|
141
|
+
method: "POST",
|
|
142
|
+
url: "/api/partitions",
|
|
143
|
+
payload: { name: "Bare Partition" },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(res.statusCode).toBe(201);
|
|
147
|
+
const body = JSON.parse(res.payload);
|
|
148
|
+
expect(body.partition.name).toBe("Bare Partition");
|
|
149
|
+
expect(body.partition.variables).toEqual({});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns 400 for missing name", async () => {
|
|
153
|
+
const res = await ctx.app.inject({
|
|
154
|
+
method: "POST",
|
|
155
|
+
url: "/api/partitions",
|
|
156
|
+
payload: {},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(res.statusCode).toBe(400);
|
|
160
|
+
const body = JSON.parse(res.payload);
|
|
161
|
+
expect(body.error).toBe("Invalid input");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns 400 for empty name", async () => {
|
|
165
|
+
const res = await ctx.app.inject({
|
|
166
|
+
method: "POST",
|
|
167
|
+
url: "/api/partitions",
|
|
168
|
+
payload: { name: "" },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(res.statusCode).toBe(400);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("trims whitespace from name", async () => {
|
|
175
|
+
const res = await ctx.app.inject({
|
|
176
|
+
method: "POST",
|
|
177
|
+
url: "/api/partitions",
|
|
178
|
+
payload: { name: " Padded Name " },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(res.statusCode).toBe(201);
|
|
182
|
+
const body = JSON.parse(res.payload);
|
|
183
|
+
expect(body.partition.name).toBe("Padded Name");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// --- GET /api/partitions ---
|
|
188
|
+
|
|
189
|
+
describe("GET /api/partitions", () => {
|
|
190
|
+
it("returns empty list when no partitions exist", async () => {
|
|
191
|
+
const res = await ctx.app.inject({
|
|
192
|
+
method: "GET",
|
|
193
|
+
url: "/api/partitions",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(res.statusCode).toBe(200);
|
|
197
|
+
const body = JSON.parse(res.payload);
|
|
198
|
+
expect(body.partitions).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("returns all partitions", async () => {
|
|
202
|
+
ctx.partitions.create("Alpha");
|
|
203
|
+
ctx.partitions.create("Beta");
|
|
204
|
+
|
|
205
|
+
const res = await ctx.app.inject({
|
|
206
|
+
method: "GET",
|
|
207
|
+
url: "/api/partitions",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(res.statusCode).toBe(200);
|
|
211
|
+
const body = JSON.parse(res.payload);
|
|
212
|
+
expect(body.partitions).toHaveLength(2);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// --- GET /api/partitions/:id ---
|
|
217
|
+
|
|
218
|
+
describe("GET /api/partitions/:id", () => {
|
|
219
|
+
it("returns a specific partition by ID", async () => {
|
|
220
|
+
const partition = ctx.partitions.create("Acme Corp", { KEY: "val" });
|
|
221
|
+
|
|
222
|
+
const res = await ctx.app.inject({
|
|
223
|
+
method: "GET",
|
|
224
|
+
url: `/api/partitions/${partition.id}`,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(res.statusCode).toBe(200);
|
|
228
|
+
const body = JSON.parse(res.payload);
|
|
229
|
+
expect(body.partition.id).toBe(partition.id);
|
|
230
|
+
expect(body.partition.name).toBe("Acme Corp");
|
|
231
|
+
expect(body.partition.variables.KEY).toBe("val");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("returns 404 for non-existent partition", async () => {
|
|
235
|
+
const res = await ctx.app.inject({
|
|
236
|
+
method: "GET",
|
|
237
|
+
url: "/api/partitions/does-not-exist",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(res.statusCode).toBe(404);
|
|
241
|
+
const body = JSON.parse(res.payload);
|
|
242
|
+
expect(body.error).toBe("Partition not found");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// --- PUT /api/partitions/:id ---
|
|
247
|
+
|
|
248
|
+
describe("PUT /api/partitions/:id", () => {
|
|
249
|
+
it("updates partition name", async () => {
|
|
250
|
+
const partition = ctx.partitions.create("Old Name");
|
|
251
|
+
|
|
252
|
+
const res = await ctx.app.inject({
|
|
253
|
+
method: "PUT",
|
|
254
|
+
url: `/api/partitions/${partition.id}`,
|
|
255
|
+
payload: { name: "New Name" },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(res.statusCode).toBe(200);
|
|
259
|
+
const body = JSON.parse(res.payload);
|
|
260
|
+
expect(body.partition.name).toBe("New Name");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("returns 404 for non-existent partition", async () => {
|
|
264
|
+
const res = await ctx.app.inject({
|
|
265
|
+
method: "PUT",
|
|
266
|
+
url: "/api/partitions/does-not-exist",
|
|
267
|
+
payload: { name: "Irrelevant" },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(res.statusCode).toBe(404);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("returns 400 for invalid input (empty name)", async () => {
|
|
274
|
+
const partition = ctx.partitions.create("Valid");
|
|
275
|
+
|
|
276
|
+
const res = await ctx.app.inject({
|
|
277
|
+
method: "PUT",
|
|
278
|
+
url: `/api/partitions/${partition.id}`,
|
|
279
|
+
payload: { name: "" },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(res.statusCode).toBe(400);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// --- DELETE /api/partitions/:id ---
|
|
287
|
+
|
|
288
|
+
describe("DELETE /api/partitions/:id", () => {
|
|
289
|
+
it("deletes an existing partition", async () => {
|
|
290
|
+
const partition = ctx.partitions.create("To Delete");
|
|
291
|
+
|
|
292
|
+
const res = await ctx.app.inject({
|
|
293
|
+
method: "DELETE",
|
|
294
|
+
url: `/api/partitions/${partition.id}`,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(res.statusCode).toBe(200);
|
|
298
|
+
const body = JSON.parse(res.payload);
|
|
299
|
+
expect(body.deleted).toBe(true);
|
|
300
|
+
|
|
301
|
+
// Verify it's gone
|
|
302
|
+
expect(ctx.partitions.get(partition.id)).toBeUndefined();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("returns 404 for non-existent partition", async () => {
|
|
306
|
+
const res = await ctx.app.inject({
|
|
307
|
+
method: "DELETE",
|
|
308
|
+
url: "/api/partitions/does-not-exist",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(res.statusCode).toBe(404);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// --- PUT /api/partitions/:id/variables ---
|
|
316
|
+
|
|
317
|
+
describe("PUT /api/partitions/:id/variables", () => {
|
|
318
|
+
it("sets variables on a partition", async () => {
|
|
319
|
+
const partition = ctx.partitions.create("Acme");
|
|
320
|
+
|
|
321
|
+
const res = await ctx.app.inject({
|
|
322
|
+
method: "PUT",
|
|
323
|
+
url: `/api/partitions/${partition.id}/variables`,
|
|
324
|
+
payload: { variables: { DB_HOST: "new-db", LOG_LEVEL: "debug" } },
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(res.statusCode).toBe(200);
|
|
328
|
+
const body = JSON.parse(res.payload);
|
|
329
|
+
expect(body.partition.variables.DB_HOST).toBe("new-db");
|
|
330
|
+
expect(body.partition.variables.LOG_LEVEL).toBe("debug");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("returns 404 for non-existent partition", async () => {
|
|
334
|
+
const res = await ctx.app.inject({
|
|
335
|
+
method: "PUT",
|
|
336
|
+
url: "/api/partitions/does-not-exist/variables",
|
|
337
|
+
payload: { variables: { KEY: "val" } },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(res.statusCode).toBe(404);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("returns 400 for invalid input", async () => {
|
|
344
|
+
const partition = ctx.partitions.create("Acme");
|
|
345
|
+
|
|
346
|
+
const res = await ctx.app.inject({
|
|
347
|
+
method: "PUT",
|
|
348
|
+
url: `/api/partitions/${partition.id}/variables`,
|
|
349
|
+
payload: {},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(res.statusCode).toBe(400);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// --- GET /api/partitions/:id/history ---
|
|
357
|
+
|
|
358
|
+
describe("GET /api/partitions/:id/history", () => {
|
|
359
|
+
it("returns history for a partition", async () => {
|
|
360
|
+
const partition = ctx.partitions.create("Acme");
|
|
361
|
+
|
|
362
|
+
const res = await ctx.app.inject({
|
|
363
|
+
method: "GET",
|
|
364
|
+
url: `/api/partitions/${partition.id}/history`,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(res.statusCode).toBe(200);
|
|
368
|
+
const body = JSON.parse(res.payload);
|
|
369
|
+
expect(body.history).toBeDefined();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("returns 404 for non-existent partition", async () => {
|
|
373
|
+
const res = await ctx.app.inject({
|
|
374
|
+
method: "GET",
|
|
375
|
+
url: "/api/partitions/does-not-exist/history",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(res.statusCode).toBe(404);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
// Environment Routes
|
|
385
|
+
// ===========================================================================
|
|
386
|
+
|
|
387
|
+
describe("Environment Routes", () => {
|
|
388
|
+
let ctx: TestContext;
|
|
389
|
+
|
|
390
|
+
beforeEach(async () => {
|
|
391
|
+
ctx = await createTestServer();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// --- POST /api/environments ---
|
|
395
|
+
|
|
396
|
+
describe("POST /api/environments", () => {
|
|
397
|
+
it("creates an environment and returns 201", async () => {
|
|
398
|
+
const res = await ctx.app.inject({
|
|
399
|
+
method: "POST",
|
|
400
|
+
url: "/api/environments",
|
|
401
|
+
payload: { name: "production", variables: { APP_ENV: "production" } },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(res.statusCode).toBe(201);
|
|
405
|
+
const body = JSON.parse(res.payload);
|
|
406
|
+
expect(body.environment).toBeDefined();
|
|
407
|
+
expect(body.environment.name).toBe("production");
|
|
408
|
+
expect(body.environment.variables.APP_ENV).toBe("production");
|
|
409
|
+
expect(body.environment.id).toBeDefined();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("creates an environment without variables", async () => {
|
|
413
|
+
const res = await ctx.app.inject({
|
|
414
|
+
method: "POST",
|
|
415
|
+
url: "/api/environments",
|
|
416
|
+
payload: { name: "bare-env" },
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(res.statusCode).toBe(201);
|
|
420
|
+
const body = JSON.parse(res.payload);
|
|
421
|
+
expect(body.environment.variables).toEqual({});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("returns 400 for missing name", async () => {
|
|
425
|
+
const res = await ctx.app.inject({
|
|
426
|
+
method: "POST",
|
|
427
|
+
url: "/api/environments",
|
|
428
|
+
payload: {},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(res.statusCode).toBe(400);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("returns 400 for empty name", async () => {
|
|
435
|
+
const res = await ctx.app.inject({
|
|
436
|
+
method: "POST",
|
|
437
|
+
url: "/api/environments",
|
|
438
|
+
payload: { name: "" },
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
expect(res.statusCode).toBe(400);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// --- GET /api/environments ---
|
|
446
|
+
|
|
447
|
+
describe("GET /api/environments", () => {
|
|
448
|
+
it("returns empty list when no environments exist", async () => {
|
|
449
|
+
const res = await ctx.app.inject({
|
|
450
|
+
method: "GET",
|
|
451
|
+
url: "/api/environments",
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(res.statusCode).toBe(200);
|
|
455
|
+
const body = JSON.parse(res.payload);
|
|
456
|
+
expect(body.environments).toEqual([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("returns all environments", async () => {
|
|
460
|
+
ctx.environments.create("production");
|
|
461
|
+
ctx.environments.create("staging");
|
|
462
|
+
|
|
463
|
+
const res = await ctx.app.inject({
|
|
464
|
+
method: "GET",
|
|
465
|
+
url: "/api/environments",
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(res.statusCode).toBe(200);
|
|
469
|
+
const body = JSON.parse(res.payload);
|
|
470
|
+
expect(body.environments).toHaveLength(2);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// --- GET /api/environments/:id ---
|
|
475
|
+
|
|
476
|
+
describe("GET /api/environments/:id", () => {
|
|
477
|
+
it("returns a specific environment", async () => {
|
|
478
|
+
const env = ctx.environments.create("production", { KEY: "val" });
|
|
479
|
+
|
|
480
|
+
const res = await ctx.app.inject({
|
|
481
|
+
method: "GET",
|
|
482
|
+
url: `/api/environments/${env.id}`,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(res.statusCode).toBe(200);
|
|
486
|
+
const body = JSON.parse(res.payload);
|
|
487
|
+
expect(body.environment.id).toBe(env.id);
|
|
488
|
+
expect(body.environment.name).toBe("production");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("returns 404 for non-existent environment", async () => {
|
|
492
|
+
const res = await ctx.app.inject({
|
|
493
|
+
method: "GET",
|
|
494
|
+
url: "/api/environments/does-not-exist",
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(res.statusCode).toBe(404);
|
|
498
|
+
const body = JSON.parse(res.payload);
|
|
499
|
+
expect(body.error).toBe("Environment not found");
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// --- PUT /api/environments/:id ---
|
|
504
|
+
|
|
505
|
+
describe("PUT /api/environments/:id", () => {
|
|
506
|
+
it("updates environment name", async () => {
|
|
507
|
+
const env = ctx.environments.create("old-name");
|
|
508
|
+
|
|
509
|
+
const res = await ctx.app.inject({
|
|
510
|
+
method: "PUT",
|
|
511
|
+
url: `/api/environments/${env.id}`,
|
|
512
|
+
payload: { name: "new-name" },
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(res.statusCode).toBe(200);
|
|
516
|
+
const body = JSON.parse(res.payload);
|
|
517
|
+
expect(body.environment.name).toBe("new-name");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("updates environment variables", async () => {
|
|
521
|
+
const env = ctx.environments.create("production", { OLD_KEY: "old" });
|
|
522
|
+
|
|
523
|
+
const res = await ctx.app.inject({
|
|
524
|
+
method: "PUT",
|
|
525
|
+
url: `/api/environments/${env.id}`,
|
|
526
|
+
payload: { variables: { NEW_KEY: "new" } },
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
expect(res.statusCode).toBe(200);
|
|
530
|
+
const body = JSON.parse(res.payload);
|
|
531
|
+
expect(body.environment.variables.NEW_KEY).toBe("new");
|
|
532
|
+
// Existing variables are merged
|
|
533
|
+
expect(body.environment.variables.OLD_KEY).toBe("old");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("returns 404 for non-existent environment", async () => {
|
|
537
|
+
const res = await ctx.app.inject({
|
|
538
|
+
method: "PUT",
|
|
539
|
+
url: "/api/environments/does-not-exist",
|
|
540
|
+
payload: { name: "irrelevant" },
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
expect(res.statusCode).toBe(404);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("returns 400 for invalid input (empty name)", async () => {
|
|
547
|
+
const env = ctx.environments.create("valid");
|
|
548
|
+
|
|
549
|
+
const res = await ctx.app.inject({
|
|
550
|
+
method: "PUT",
|
|
551
|
+
url: `/api/environments/${env.id}`,
|
|
552
|
+
payload: { name: "" },
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
expect(res.statusCode).toBe(400);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// --- DELETE /api/environments/:id ---
|
|
560
|
+
|
|
561
|
+
describe("DELETE /api/environments/:id", () => {
|
|
562
|
+
it("deletes an environment with no deployments", async () => {
|
|
563
|
+
const env = ctx.environments.create("to-delete");
|
|
564
|
+
|
|
565
|
+
const res = await ctx.app.inject({
|
|
566
|
+
method: "DELETE",
|
|
567
|
+
url: `/api/environments/${env.id}`,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
expect(res.statusCode).toBe(200);
|
|
571
|
+
const body = JSON.parse(res.payload);
|
|
572
|
+
expect(body.deleted).toBe(true);
|
|
573
|
+
expect(ctx.environments.get(env.id)).toBeUndefined();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("returns 404 for non-existent environment", async () => {
|
|
577
|
+
const res = await ctx.app.inject({
|
|
578
|
+
method: "DELETE",
|
|
579
|
+
url: "/api/environments/does-not-exist",
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(res.statusCode).toBe(404);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("returns 409 when environment has deployments", async () => {
|
|
586
|
+
const env = ctx.environments.create("production");
|
|
587
|
+
const artifact = ctx.artifactStore.create({
|
|
588
|
+
name: "web-app",
|
|
589
|
+
type: "nodejs",
|
|
590
|
+
analysis: { summary: "test", dependencies: [], configurationExpectations: {}, deploymentIntent: "rolling", confidence: 0.9 },
|
|
591
|
+
annotations: [],
|
|
592
|
+
learningHistory: [],
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Create a deployment linked to this environment
|
|
596
|
+
await deployViaHttp(ctx.app, {
|
|
597
|
+
artifactId: artifact.id,
|
|
598
|
+
environmentId: env.id,
|
|
599
|
+
version: "1.0.0",
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const res = await ctx.app.inject({
|
|
603
|
+
method: "DELETE",
|
|
604
|
+
url: `/api/environments/${env.id}`,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
expect(res.statusCode).toBe(409);
|
|
608
|
+
const body = JSON.parse(res.payload);
|
|
609
|
+
expect(body.error).toContain("deployment");
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// ===========================================================================
|
|
615
|
+
// Settings Routes
|
|
616
|
+
// ===========================================================================
|
|
617
|
+
|
|
618
|
+
describe("Settings Routes", () => {
|
|
619
|
+
let ctx: TestContext;
|
|
620
|
+
|
|
621
|
+
beforeEach(async () => {
|
|
622
|
+
ctx = await createTestServer();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// --- GET /api/settings ---
|
|
626
|
+
|
|
627
|
+
describe("GET /api/settings", () => {
|
|
628
|
+
it("returns default settings", async () => {
|
|
629
|
+
const res = await ctx.app.inject({
|
|
630
|
+
method: "GET",
|
|
631
|
+
url: "/api/settings",
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
expect(res.statusCode).toBe(200);
|
|
635
|
+
const body = JSON.parse(res.payload);
|
|
636
|
+
expect(body.settings).toBeDefined();
|
|
637
|
+
expect(body.settings.environmentsEnabled).toBe(true);
|
|
638
|
+
expect(body.settings.agent).toBeDefined();
|
|
639
|
+
expect(body.settings.agent.conflictPolicy).toBe("permissive");
|
|
640
|
+
expect(body.settings.deploymentDefaults).toBeDefined();
|
|
641
|
+
expect(body.settings.envoy).toBeDefined();
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// --- PUT /api/settings ---
|
|
646
|
+
|
|
647
|
+
describe("PUT /api/settings", () => {
|
|
648
|
+
it("updates environmentsEnabled", async () => {
|
|
649
|
+
const res = await ctx.app.inject({
|
|
650
|
+
method: "PUT",
|
|
651
|
+
url: "/api/settings",
|
|
652
|
+
payload: { environmentsEnabled: false },
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
expect(res.statusCode).toBe(200);
|
|
656
|
+
const body = JSON.parse(res.payload);
|
|
657
|
+
expect(body.settings.environmentsEnabled).toBe(false);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("updates agent settings", async () => {
|
|
661
|
+
const res = await ctx.app.inject({
|
|
662
|
+
method: "PUT",
|
|
663
|
+
url: "/api/settings",
|
|
664
|
+
payload: {
|
|
665
|
+
agent: {
|
|
666
|
+
conflictPolicy: "strict",
|
|
667
|
+
defaultTimeoutMs: 60000,
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
expect(res.statusCode).toBe(200);
|
|
673
|
+
const body = JSON.parse(res.payload);
|
|
674
|
+
expect(body.settings.agent.conflictPolicy).toBe("strict");
|
|
675
|
+
expect(body.settings.agent.defaultTimeoutMs).toBe(60000);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("updates envoy settings", async () => {
|
|
679
|
+
const res = await ctx.app.inject({
|
|
680
|
+
method: "PUT",
|
|
681
|
+
url: "/api/settings",
|
|
682
|
+
payload: {
|
|
683
|
+
envoy: {
|
|
684
|
+
url: "http://envoy:9411",
|
|
685
|
+
timeoutMs: 5000,
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
expect(res.statusCode).toBe(200);
|
|
691
|
+
const body = JSON.parse(res.payload);
|
|
692
|
+
expect(body.settings.envoy.url).toBe("http://envoy:9411");
|
|
693
|
+
expect(body.settings.envoy.timeoutMs).toBe(5000);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("returns 400 for invalid settings", async () => {
|
|
697
|
+
const res = await ctx.app.inject({
|
|
698
|
+
method: "PUT",
|
|
699
|
+
url: "/api/settings",
|
|
700
|
+
payload: {
|
|
701
|
+
agent: {
|
|
702
|
+
conflictPolicy: "invalid-policy",
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
expect(res.statusCode).toBe(400);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("returns 400 for invalid envoy URL", async () => {
|
|
711
|
+
const res = await ctx.app.inject({
|
|
712
|
+
method: "PUT",
|
|
713
|
+
url: "/api/settings",
|
|
714
|
+
payload: {
|
|
715
|
+
envoy: {
|
|
716
|
+
url: "not-a-url",
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
expect(res.statusCode).toBe(400);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// --- GET /api/settings/command-info ---
|
|
726
|
+
|
|
727
|
+
describe("GET /api/settings/command-info", () => {
|
|
728
|
+
it("returns command info with version and timing", async () => {
|
|
729
|
+
const res = await ctx.app.inject({
|
|
730
|
+
method: "GET",
|
|
731
|
+
url: "/api/settings/command-info",
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
expect(res.statusCode).toBe(200);
|
|
735
|
+
const body = JSON.parse(res.payload);
|
|
736
|
+
expect(body.info).toBeDefined();
|
|
737
|
+
expect(body.info.version).toBe("0.1.0");
|
|
738
|
+
expect(body.info.startedAt).toBeDefined();
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// ===========================================================================
|
|
744
|
+
// Deployment Routes
|
|
745
|
+
// ===========================================================================
|
|
746
|
+
|
|
747
|
+
describe("Deployment Routes", () => {
|
|
748
|
+
let ctx: TestContext;
|
|
749
|
+
|
|
750
|
+
beforeEach(async () => {
|
|
751
|
+
ctx = await createTestServer();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
/** Helper: create artifact in the store and return its ID */
|
|
755
|
+
function seedArtifact(name = "web-app"): string {
|
|
756
|
+
const artifact = ctx.artifactStore.create({
|
|
757
|
+
name,
|
|
758
|
+
type: "nodejs",
|
|
759
|
+
analysis: { summary: "test", dependencies: [], configurationExpectations: {}, deploymentIntent: "rolling", confidence: 0.9 },
|
|
760
|
+
annotations: [],
|
|
761
|
+
learningHistory: [],
|
|
762
|
+
});
|
|
763
|
+
return artifact.id;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// --- POST /api/deployments ---
|
|
767
|
+
|
|
768
|
+
describe("POST /api/deployments", () => {
|
|
769
|
+
it("creates a deployment and returns 201", async () => {
|
|
770
|
+
const env = ctx.environments.create("production", { APP_ENV: "production" });
|
|
771
|
+
const partition = ctx.partitions.create("Acme", { DB_HOST: "acme-db" });
|
|
772
|
+
const artifactId = seedArtifact();
|
|
773
|
+
|
|
774
|
+
const res = await deployViaHttp(ctx.app, {
|
|
775
|
+
artifactId,
|
|
776
|
+
partitionId: partition.id,
|
|
777
|
+
environmentId: env.id,
|
|
778
|
+
version: "1.0.0",
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
expect(res.statusCode).toBe(201);
|
|
782
|
+
const body = JSON.parse(res.payload);
|
|
783
|
+
expect(body.deployment).toBeDefined();
|
|
784
|
+
expect(body.deployment.artifactId).toBe(artifactId);
|
|
785
|
+
expect(body.deployment.partitionId).toBe(partition.id);
|
|
786
|
+
expect(body.deployment.version).toBe("1.0.0");
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("returns 400 for invalid trigger", async () => {
|
|
790
|
+
const res = await ctx.app.inject({
|
|
791
|
+
method: "POST",
|
|
792
|
+
url: "/api/deployments",
|
|
793
|
+
payload: {},
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
expect(res.statusCode).toBe(400);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("returns 404 when artifact does not exist", async () => {
|
|
800
|
+
const env = ctx.environments.create("production");
|
|
801
|
+
|
|
802
|
+
const res = await ctx.app.inject({
|
|
803
|
+
method: "POST",
|
|
804
|
+
url: "/api/deployments",
|
|
805
|
+
payload: {
|
|
806
|
+
artifactId: "nonexistent-artifact",
|
|
807
|
+
environmentId: env.id,
|
|
808
|
+
version: "1.0.0",
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
expect(res.statusCode).toBe(404);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it("returns 404 when environment does not exist", async () => {
|
|
816
|
+
const artifactId = seedArtifact();
|
|
817
|
+
|
|
818
|
+
const res = await ctx.app.inject({
|
|
819
|
+
method: "POST",
|
|
820
|
+
url: "/api/deployments",
|
|
821
|
+
payload: {
|
|
822
|
+
artifactId,
|
|
823
|
+
environmentId: "nonexistent",
|
|
824
|
+
version: "1.0.0",
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
expect(res.statusCode).toBe(404);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it("returns 404 when partition does not exist", async () => {
|
|
832
|
+
const env = ctx.environments.create("production");
|
|
833
|
+
const artifactId = seedArtifact();
|
|
834
|
+
|
|
835
|
+
const res = await ctx.app.inject({
|
|
836
|
+
method: "POST",
|
|
837
|
+
url: "/api/deployments",
|
|
838
|
+
payload: {
|
|
839
|
+
artifactId,
|
|
840
|
+
environmentId: env.id,
|
|
841
|
+
partitionId: "nonexistent",
|
|
842
|
+
version: "1.0.0",
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
expect(res.statusCode).toBe(404);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// --- GET /api/deployments ---
|
|
851
|
+
|
|
852
|
+
describe("GET /api/deployments", () => {
|
|
853
|
+
it("returns empty list when no deployments exist", async () => {
|
|
854
|
+
const res = await ctx.app.inject({
|
|
855
|
+
method: "GET",
|
|
856
|
+
url: "/api/deployments",
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
expect(res.statusCode).toBe(200);
|
|
860
|
+
const body = JSON.parse(res.payload);
|
|
861
|
+
expect(body.deployments).toEqual([]);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("returns deployments after creating one", async () => {
|
|
865
|
+
const env = ctx.environments.create("production");
|
|
866
|
+
const partition = ctx.partitions.create("Acme");
|
|
867
|
+
const artifactId = seedArtifact();
|
|
868
|
+
|
|
869
|
+
await deployViaHttp(ctx.app, {
|
|
870
|
+
artifactId,
|
|
871
|
+
partitionId: partition.id,
|
|
872
|
+
environmentId: env.id,
|
|
873
|
+
version: "1.0.0",
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
const res = await ctx.app.inject({
|
|
877
|
+
method: "GET",
|
|
878
|
+
url: "/api/deployments",
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
expect(res.statusCode).toBe(200);
|
|
882
|
+
const body = JSON.parse(res.payload);
|
|
883
|
+
expect(body.deployments.length).toBeGreaterThanOrEqual(1);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it("filters deployments by partitionId", async () => {
|
|
887
|
+
const env = ctx.environments.create("production");
|
|
888
|
+
const p1 = ctx.partitions.create("Acme");
|
|
889
|
+
const p2 = ctx.partitions.create("Beta");
|
|
890
|
+
const artifactId = seedArtifact();
|
|
891
|
+
|
|
892
|
+
await deployViaHttp(ctx.app, {
|
|
893
|
+
artifactId,
|
|
894
|
+
partitionId: p1.id,
|
|
895
|
+
environmentId: env.id,
|
|
896
|
+
version: "1.0.0",
|
|
897
|
+
});
|
|
898
|
+
await deployViaHttp(ctx.app, {
|
|
899
|
+
artifactId,
|
|
900
|
+
partitionId: p2.id,
|
|
901
|
+
environmentId: env.id,
|
|
902
|
+
version: "1.0.0",
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
const res = await ctx.app.inject({
|
|
906
|
+
method: "GET",
|
|
907
|
+
url: `/api/deployments?partitionId=${p1.id}`,
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
expect(res.statusCode).toBe(200);
|
|
911
|
+
const body = JSON.parse(res.payload);
|
|
912
|
+
expect(body.deployments).toHaveLength(1);
|
|
913
|
+
expect(body.deployments[0].partitionId).toBe(p1.id);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it("filters deployments by artifactId", async () => {
|
|
917
|
+
const env = ctx.environments.create("production");
|
|
918
|
+
const partition = ctx.partitions.create("Acme");
|
|
919
|
+
const art1 = seedArtifact("web-app");
|
|
920
|
+
const art2 = seedArtifact("api-service");
|
|
921
|
+
|
|
922
|
+
await deployViaHttp(ctx.app, {
|
|
923
|
+
artifactId: art1,
|
|
924
|
+
partitionId: partition.id,
|
|
925
|
+
environmentId: env.id,
|
|
926
|
+
version: "1.0.0",
|
|
927
|
+
});
|
|
928
|
+
await deployViaHttp(ctx.app, {
|
|
929
|
+
artifactId: art2,
|
|
930
|
+
partitionId: partition.id,
|
|
931
|
+
environmentId: env.id,
|
|
932
|
+
version: "1.0.0",
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const res = await ctx.app.inject({
|
|
936
|
+
method: "GET",
|
|
937
|
+
url: `/api/deployments?artifactId=${art1}`,
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
expect(res.statusCode).toBe(200);
|
|
941
|
+
const body = JSON.parse(res.payload);
|
|
942
|
+
expect(body.deployments).toHaveLength(1);
|
|
943
|
+
expect(body.deployments[0].artifactId).toBe(art1);
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
// --- GET /api/deployments/:id ---
|
|
948
|
+
|
|
949
|
+
describe("GET /api/deployments/:id", () => {
|
|
950
|
+
it("returns a deployment with debrief entries", async () => {
|
|
951
|
+
const env = ctx.environments.create("production");
|
|
952
|
+
const partition = ctx.partitions.create("Acme");
|
|
953
|
+
const artifactId = seedArtifact();
|
|
954
|
+
|
|
955
|
+
const triggerRes = await deployViaHttp(ctx.app, {
|
|
956
|
+
artifactId,
|
|
957
|
+
partitionId: partition.id,
|
|
958
|
+
environmentId: env.id,
|
|
959
|
+
version: "1.0.0",
|
|
960
|
+
});
|
|
961
|
+
const deploymentId = JSON.parse(triggerRes.payload).deployment.id;
|
|
962
|
+
|
|
963
|
+
const res = await ctx.app.inject({
|
|
964
|
+
method: "GET",
|
|
965
|
+
url: `/api/deployments/${deploymentId}`,
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
expect(res.statusCode).toBe(200);
|
|
969
|
+
const body = JSON.parse(res.payload);
|
|
970
|
+
expect(body.deployment.id).toBe(deploymentId);
|
|
971
|
+
expect(body.debrief).toBeDefined();
|
|
972
|
+
expect(Array.isArray(body.debrief)).toBe(true);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it("returns 404 for non-existent deployment", async () => {
|
|
976
|
+
const res = await ctx.app.inject({
|
|
977
|
+
method: "GET",
|
|
978
|
+
url: "/api/deployments/does-not-exist",
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
expect(res.statusCode).toBe(404);
|
|
982
|
+
const body = JSON.parse(res.payload);
|
|
983
|
+
expect(body.error).toBe("Deployment not found");
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// --- GET /api/deployments/:id/postmortem ---
|
|
988
|
+
|
|
989
|
+
describe("GET /api/deployments/:id/postmortem", () => {
|
|
990
|
+
it("returns a postmortem for a deployment", async () => {
|
|
991
|
+
const env = ctx.environments.create("production");
|
|
992
|
+
const partition = ctx.partitions.create("Acme");
|
|
993
|
+
const artifactId = seedArtifact();
|
|
994
|
+
|
|
995
|
+
const triggerRes = await deployViaHttp(ctx.app, {
|
|
996
|
+
artifactId,
|
|
997
|
+
partitionId: partition.id,
|
|
998
|
+
environmentId: env.id,
|
|
999
|
+
version: "1.0.0",
|
|
1000
|
+
});
|
|
1001
|
+
const deploymentId = JSON.parse(triggerRes.payload).deployment.id;
|
|
1002
|
+
|
|
1003
|
+
const res = await ctx.app.inject({
|
|
1004
|
+
method: "GET",
|
|
1005
|
+
url: `/api/deployments/${deploymentId}/postmortem`,
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
expect(res.statusCode).toBe(200);
|
|
1009
|
+
const body = JSON.parse(res.payload);
|
|
1010
|
+
expect(body.postmortem).toBeDefined();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it("returns 404 for non-existent deployment", async () => {
|
|
1014
|
+
const res = await ctx.app.inject({
|
|
1015
|
+
method: "GET",
|
|
1016
|
+
url: "/api/deployments/does-not-exist/postmortem",
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
expect(res.statusCode).toBe(404);
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// --- GET /api/debrief ---
|
|
1024
|
+
|
|
1025
|
+
describe("GET /api/debrief", () => {
|
|
1026
|
+
it("returns recent debrief entries", async () => {
|
|
1027
|
+
// Trigger a deployment to generate debrief entries
|
|
1028
|
+
const env = ctx.environments.create("production");
|
|
1029
|
+
const partition = ctx.partitions.create("Acme");
|
|
1030
|
+
const artifactId = seedArtifact();
|
|
1031
|
+
|
|
1032
|
+
await deployViaHttp(ctx.app, {
|
|
1033
|
+
artifactId,
|
|
1034
|
+
partitionId: partition.id,
|
|
1035
|
+
environmentId: env.id,
|
|
1036
|
+
version: "1.0.0",
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
const res = await ctx.app.inject({
|
|
1040
|
+
method: "GET",
|
|
1041
|
+
url: "/api/debrief",
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
expect(res.statusCode).toBe(200);
|
|
1045
|
+
const body = JSON.parse(res.payload);
|
|
1046
|
+
expect(body.entries).toBeDefined();
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it("respects limit parameter", async () => {
|
|
1050
|
+
const env = ctx.environments.create("production");
|
|
1051
|
+
const partition = ctx.partitions.create("Acme");
|
|
1052
|
+
const artifactId = seedArtifact();
|
|
1053
|
+
|
|
1054
|
+
await deployViaHttp(ctx.app, {
|
|
1055
|
+
artifactId,
|
|
1056
|
+
partitionId: partition.id,
|
|
1057
|
+
environmentId: env.id,
|
|
1058
|
+
version: "1.0.0",
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
const res = await ctx.app.inject({
|
|
1062
|
+
method: "GET",
|
|
1063
|
+
url: "/api/debrief?limit=2",
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
expect(res.statusCode).toBe(200);
|
|
1067
|
+
const body = JSON.parse(res.payload);
|
|
1068
|
+
expect(body.entries.length).toBeLessThanOrEqual(2);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it("filters by partitionId", async () => {
|
|
1072
|
+
const env = ctx.environments.create("production");
|
|
1073
|
+
const p1 = ctx.partitions.create("Acme");
|
|
1074
|
+
const p2 = ctx.partitions.create("Beta");
|
|
1075
|
+
const artifactId = seedArtifact();
|
|
1076
|
+
|
|
1077
|
+
await deployViaHttp(ctx.app, {
|
|
1078
|
+
artifactId,
|
|
1079
|
+
partitionId: p1.id,
|
|
1080
|
+
environmentId: env.id,
|
|
1081
|
+
version: "1.0.0",
|
|
1082
|
+
});
|
|
1083
|
+
await deployViaHttp(ctx.app, {
|
|
1084
|
+
artifactId,
|
|
1085
|
+
partitionId: p2.id,
|
|
1086
|
+
environmentId: env.id,
|
|
1087
|
+
version: "1.0.0",
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
const res = await ctx.app.inject({
|
|
1091
|
+
method: "GET",
|
|
1092
|
+
url: `/api/debrief?partitionId=${p1.id}`,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
expect(res.statusCode).toBe(200);
|
|
1096
|
+
const body = JSON.parse(res.payload);
|
|
1097
|
+
for (const entry of body.entries) {
|
|
1098
|
+
expect(entry.partitionId).toBe(p1.id);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
// ===========================================================================
|
|
1105
|
+
// Artifact Routes
|
|
1106
|
+
// ===========================================================================
|
|
1107
|
+
|
|
1108
|
+
describe("Artifact Routes", () => {
|
|
1109
|
+
let ctx: TestContext;
|
|
1110
|
+
|
|
1111
|
+
beforeEach(async () => {
|
|
1112
|
+
ctx = await createTestServer();
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
describe("POST /api/artifacts", () => {
|
|
1116
|
+
it("creates an artifact and returns 201", async () => {
|
|
1117
|
+
const res = await ctx.app.inject({
|
|
1118
|
+
method: "POST",
|
|
1119
|
+
url: "/api/artifacts",
|
|
1120
|
+
payload: { name: "web-app", type: "nodejs" },
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
expect(res.statusCode).toBe(201);
|
|
1124
|
+
const body = JSON.parse(res.payload);
|
|
1125
|
+
expect(body.artifact).toBeDefined();
|
|
1126
|
+
expect(body.artifact.name).toBe("web-app");
|
|
1127
|
+
expect(body.artifact.type).toBe("nodejs");
|
|
1128
|
+
expect(body.artifact.id).toBeDefined();
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("returns 400 for missing name", async () => {
|
|
1132
|
+
const res = await ctx.app.inject({
|
|
1133
|
+
method: "POST",
|
|
1134
|
+
url: "/api/artifacts",
|
|
1135
|
+
payload: { type: "nodejs" },
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
expect(res.statusCode).toBe(400);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it("returns 400 for missing type", async () => {
|
|
1142
|
+
const res = await ctx.app.inject({
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
url: "/api/artifacts",
|
|
1145
|
+
payload: { name: "web-app" },
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
expect(res.statusCode).toBe(400);
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
describe("GET /api/artifacts", () => {
|
|
1153
|
+
it("returns empty list when no artifacts exist", async () => {
|
|
1154
|
+
const res = await ctx.app.inject({
|
|
1155
|
+
method: "GET",
|
|
1156
|
+
url: "/api/artifacts",
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
expect(res.statusCode).toBe(200);
|
|
1160
|
+
const body = JSON.parse(res.payload);
|
|
1161
|
+
expect(body.artifacts).toEqual([]);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it("returns all artifacts", async () => {
|
|
1165
|
+
ctx.artifactStore.create({
|
|
1166
|
+
name: "web-app", type: "nodejs",
|
|
1167
|
+
analysis: { summary: "test", dependencies: [], configurationExpectations: {}, deploymentIntent: "rolling", confidence: 0.9 },
|
|
1168
|
+
annotations: [], learningHistory: [],
|
|
1169
|
+
});
|
|
1170
|
+
ctx.artifactStore.create({
|
|
1171
|
+
name: "api-service", type: "docker",
|
|
1172
|
+
analysis: { summary: "test", dependencies: [], configurationExpectations: {}, deploymentIntent: "blue-green", confidence: 0.8 },
|
|
1173
|
+
annotations: [], learningHistory: [],
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
const res = await ctx.app.inject({
|
|
1177
|
+
method: "GET",
|
|
1178
|
+
url: "/api/artifacts",
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
expect(res.statusCode).toBe(200);
|
|
1182
|
+
const body = JSON.parse(res.payload);
|
|
1183
|
+
expect(body.artifacts).toHaveLength(2);
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
describe("GET /api/artifacts/:id", () => {
|
|
1188
|
+
it("returns a specific artifact", async () => {
|
|
1189
|
+
const artifact = ctx.artifactStore.create({
|
|
1190
|
+
name: "web-app", type: "nodejs",
|
|
1191
|
+
analysis: { summary: "test", dependencies: [], configurationExpectations: {}, deploymentIntent: "rolling", confidence: 0.9 },
|
|
1192
|
+
annotations: [], learningHistory: [],
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
const res = await ctx.app.inject({
|
|
1196
|
+
method: "GET",
|
|
1197
|
+
url: `/api/artifacts/${artifact.id}`,
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
expect(res.statusCode).toBe(200);
|
|
1201
|
+
const body = JSON.parse(res.payload);
|
|
1202
|
+
expect(body.artifact.id).toBe(artifact.id);
|
|
1203
|
+
expect(body.artifact.name).toBe("web-app");
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it("returns 404 for non-existent artifact", async () => {
|
|
1207
|
+
const res = await ctx.app.inject({
|
|
1208
|
+
method: "GET",
|
|
1209
|
+
url: "/api/artifacts/does-not-exist",
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
expect(res.statusCode).toBe(404);
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// ===========================================================================
|
|
1218
|
+
// Health Routes
|
|
1219
|
+
// ===========================================================================
|
|
1220
|
+
|
|
1221
|
+
describe("Health Routes", () => {
|
|
1222
|
+
let ctx: TestContext;
|
|
1223
|
+
|
|
1224
|
+
beforeEach(async () => {
|
|
1225
|
+
ctx = await createTestServer();
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
describe("GET /health", () => {
|
|
1229
|
+
it("returns healthy status", async () => {
|
|
1230
|
+
const res = await ctx.app.inject({
|
|
1231
|
+
method: "GET",
|
|
1232
|
+
url: "/health",
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
expect(res.statusCode).toBe(200);
|
|
1236
|
+
const body = JSON.parse(res.payload);
|
|
1237
|
+
expect(body.status).toBe("ok");
|
|
1238
|
+
expect(body.service).toBe("synth-server");
|
|
1239
|
+
expect(body.timestamp).toBeDefined();
|
|
1240
|
+
// Validate timestamp is a valid ISO date
|
|
1241
|
+
expect(() => new Date(body.timestamp)).not.toThrow();
|
|
1242
|
+
expect(new Date(body.timestamp).toISOString()).toBe(body.timestamp);
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
});
|