feat(FN-3110): add docker provisioning service, API routes, and status UI c
Merges FN-3110 to add Docker provisioning support across the stack: TypeScript types and DockerProvisioningService in core, API routes in dashboard for provisioning operations, a useDockerProvisioning hook for frontend integration, and a DockerProvisioningStatus UI component — all covered by unit an Fusion-Task-Id: FN-3110
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
|
||||
// Must be called before importing the hook
|
||||
const fetchMock = vi.fn();
|
||||
global.fetch = fetchMock;
|
||||
|
||||
// Import the hook after mock setup
|
||||
import { useDockerProvisioning } from "../useDockerProvisioning";
|
||||
|
||||
function jsonOk(body: unknown) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve(body),
|
||||
});
|
||||
}
|
||||
|
||||
function jsonError(status: number, body: unknown) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status,
|
||||
json: () => Promise.resolve(body),
|
||||
});
|
||||
}
|
||||
|
||||
describe("useDockerProvisioning", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("provision", () => {
|
||||
it("posts to /api/docker/provision and returns result", async () => {
|
||||
const result = {
|
||||
success: true,
|
||||
containerId: "abc",
|
||||
containerName: "fusion-test-abc12345",
|
||||
apiKey: "fn_key",
|
||||
portMapping: "4040:49152",
|
||||
durationMs: 1000,
|
||||
};
|
||||
fetchMock.mockReturnValue(jsonOk(result));
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
let provisionResult;
|
||||
await act(async () => {
|
||||
provisionResult = await hookResult.current.provision({
|
||||
nodeName: "test",
|
||||
hostConfig: {},
|
||||
imageConfig: { image: "runfusion/fusion", tag: "latest", pullImage: true },
|
||||
autoGenerateApiKey: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/docker/provision",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
expect(provisionResult!.success).toBe(true);
|
||||
expect(hookResult.current.provisionResult).toEqual(result);
|
||||
expect(hookResult.current.isProvisioning).toBe(false);
|
||||
});
|
||||
|
||||
it("sets provisionError on non-ok response", async () => {
|
||||
fetchMock.mockReturnValue(
|
||||
jsonError(500, { success: false, error: "internal error" }),
|
||||
);
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
await act(async () => {
|
||||
await hookResult.current.provision({
|
||||
nodeName: "test",
|
||||
hostConfig: {},
|
||||
imageConfig: { image: "runfusion/fusion", tag: "latest", pullImage: true },
|
||||
autoGenerateApiKey: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(hookResult.current.provisionError).toBe("internal error");
|
||||
});
|
||||
|
||||
it("sets provisionError on fetch rejection", async () => {
|
||||
fetchMock.mockRejectedValue(new Error("network error"));
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
await act(async () => {
|
||||
await hookResult.current.provision({
|
||||
nodeName: "test",
|
||||
hostConfig: {},
|
||||
imageConfig: { image: "runfusion/fusion", tag: "latest", pullImage: true },
|
||||
autoGenerateApiKey: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(hookResult.current.provisionError).toBe("network error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deprovision", () => {
|
||||
it("posts to /api/docker/deprovision with correct body", async () => {
|
||||
fetchMock.mockReturnValue(jsonOk({ success: true }));
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
await act(async () => {
|
||||
await hookResult.current.deprovision("abc123", { host: "tcp://1.2.3.4:2376" }, true);
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/docker/deprovision",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
containerId: "abc123",
|
||||
hostConfig: { host: "tcp://1.2.3.4:2376" },
|
||||
removeVolumes: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(hookResult.current.isDeprovisioning).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("startContainer", () => {
|
||||
it("posts to the correct URL", async () => {
|
||||
fetchMock.mockReturnValue(jsonOk({ success: true }));
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
await act(async () => {
|
||||
await hookResult.current.startContainer("abc123", {});
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/docker/containers/abc123/start",
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopContainer", () => {
|
||||
it("posts to the correct URL", async () => {
|
||||
fetchMock.mockReturnValue(jsonOk({ success: true }));
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
await act(async () => {
|
||||
await hookResult.current.stopContainer("abc123", {});
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/docker/containers/abc123/stop",
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("restartContainer", () => {
|
||||
it("posts to the correct URL", async () => {
|
||||
fetchMock.mockReturnValue(jsonOk({ success: true }));
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
await act(async () => {
|
||||
await hookResult.current.restartContainer("abc123", {});
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/docker/containers/abc123/restart",
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultImage", () => {
|
||||
it("fetches from /api/docker/default-image", async () => {
|
||||
fetchMock.mockReturnValue(jsonOk({ image: "runfusion/fusion", tag: "latest" }));
|
||||
|
||||
const { result: hookResult } = renderHook(() => useDockerProvisioning());
|
||||
|
||||
let result;
|
||||
await act(async () => {
|
||||
result = await hookResult.current.getDefaultImage();
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith("/api/docker/default-image");
|
||||
expect(result).toEqual({ image: "runfusion/fusion", tag: "latest" });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user