test: add comprehensive tests for tool workshop functionality
Backend tests: - Unit tests for docker_build service (successful/failed builds, context, paths) - Unit tests for readiness_probe service (success, timeout, retries, edge cases) - Integration tests for config_folders API (CRUD + project overrides) - Integration tests for tool_types API with new fields - Integration tests for tool_configs API with new fields Frontend tests: - ToolWorkshopPage component tests (all 3 tabs, create/edit/delete) - API client tests for tool_types and config_folders Fixes: - Add field_validator import to tool_configs.py - Add JSON import to tool_config model - Update frontend test button names to match UI (Create Tool Type, Add Config, Create Folder) Quality gates: backend unit tests passing (23/23)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import axios from "axios";
|
||||
import {
|
||||
createConfigFolder,
|
||||
deleteConfigFolder,
|
||||
listConfigFolders,
|
||||
updateConfigFolder,
|
||||
} from "../api/config_folders";
|
||||
|
||||
vi.mock("axios");
|
||||
const mockedAxios = vi.mocked(axios);
|
||||
|
||||
describe("config_folders API", () => {
|
||||
describe("listConfigFolders", () => {
|
||||
it("returns folders with files and overrides", async () => {
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{
|
||||
id: "folder-1",
|
||||
name: "my-dotfiles",
|
||||
mount_path: "/home/user",
|
||||
files: {
|
||||
".zshrc": "export ZSH=\"$HOME/.oh-my-zsh\"",
|
||||
},
|
||||
project_overrides: {
|
||||
"proj-1": {
|
||||
mount_path: "/workspace",
|
||||
files: { ".zshrc": "different content" },
|
||||
},
|
||||
},
|
||||
is_active: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
mockedAxios.get.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await listConfigFolders();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("my-dotfiles");
|
||||
expect(result[0].files).toEqual({
|
||||
".zshrc": "export ZSH=\"$HOME/.oh-my-zsh\"",
|
||||
});
|
||||
expect(result[0].project_overrides).toEqual({
|
||||
"proj-1": {
|
||||
mount_path: "/workspace",
|
||||
files: { ".zshrc": "different content" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty array when no folders", async () => {
|
||||
mockedAxios.get.mockResolvedValue({ data: [] });
|
||||
|
||||
const result = await listConfigFolders();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createConfigFolder", () => {
|
||||
it("creates folder with files", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: "new-folder",
|
||||
name: "my-configs",
|
||||
mount_path: "/home/user",
|
||||
files: { "test.txt": "hello" },
|
||||
is_active: true,
|
||||
},
|
||||
};
|
||||
mockedAxios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await createConfigFolder({
|
||||
name: "my-configs",
|
||||
mount_path: "/home/user",
|
||||
files: { "test.txt": "hello" },
|
||||
});
|
||||
|
||||
expect(result.name).toBe("my-configs");
|
||||
expect(result.files).toEqual({ "test.txt": "hello" });
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
"/config-folders",
|
||||
expect.objectContaining({
|
||||
name: "my-configs",
|
||||
mount_path: "/home/user",
|
||||
files: { "test.txt": "hello" },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateConfigFolder", () => {
|
||||
it("updates folder files", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: "folder-1",
|
||||
name: "updated-name",
|
||||
files: { "new.txt": "content" },
|
||||
},
|
||||
};
|
||||
mockedAxios.put.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateConfigFolder("folder-1", {
|
||||
name: "updated-name",
|
||||
files: { "new.txt": "content" },
|
||||
});
|
||||
|
||||
expect(result.name).toBe("updated-name");
|
||||
expect(mockedAxios.put).toHaveBeenCalledWith(
|
||||
"/config-folders/folder-1",
|
||||
expect.objectContaining({
|
||||
name: "updated-name",
|
||||
files: { "new.txt": "content" },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("updates folder activation status", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: "folder-1",
|
||||
name: "my-configs",
|
||||
is_active: false,
|
||||
},
|
||||
};
|
||||
mockedAxios.put.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateConfigFolder("folder-1", {
|
||||
is_active: false,
|
||||
});
|
||||
|
||||
expect(result.is_active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteConfigFolder", () => {
|
||||
it("deletes folder", async () => {
|
||||
mockedAxios.delete.mockResolvedValue({ data: undefined });
|
||||
|
||||
await deleteConfigFolder("folder-1");
|
||||
|
||||
expect(mockedAxios.delete).toHaveBeenCalledWith("/config-folders/folder-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import axios from "axios";
|
||||
import {
|
||||
createToolType,
|
||||
deleteToolType,
|
||||
listToolTypes,
|
||||
updateToolType,
|
||||
validateToolType,
|
||||
} from "../api/tool_types";
|
||||
|
||||
vi.mock("axios");
|
||||
const mockedAxios = vi.mocked(axios);
|
||||
|
||||
describe("tool_types API", () => {
|
||||
describe("listToolTypes", () => {
|
||||
it("returns tool types with new fields", async () => {
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{
|
||||
id: "type-1",
|
||||
name: "custom-tool",
|
||||
display_name: "Custom Tool",
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM python:3.11",
|
||||
readiness_probe: {
|
||||
command: "python --version",
|
||||
timeout: 30,
|
||||
interval: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
mockedAxios.get.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await listToolTypes();
|
||||
|
||||
expect(result[0].definition_type).toBe("dockerfile");
|
||||
expect(result[0].dockerfile_template).toBe("FROM python:3.11");
|
||||
expect(result[0].readiness_probe).toEqual({
|
||||
command: "python --version",
|
||||
timeout: 30,
|
||||
interval: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns compose tool types", async () => {
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{
|
||||
id: "type-1",
|
||||
name: "code-server",
|
||||
definition_type: "compose",
|
||||
compose_template: "version: '3.8'",
|
||||
dockerfile_template: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
mockedAxios.get.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await listToolTypes();
|
||||
|
||||
expect(result[0].definition_type).toBe("compose");
|
||||
expect(result[0].dockerfile_template).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createToolType", () => {
|
||||
it("creates tool type with dockerfile", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: "new-type",
|
||||
name: "docker-tool",
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM node:18",
|
||||
},
|
||||
};
|
||||
mockedAxios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await createToolType({
|
||||
name: "docker-tool",
|
||||
display_name: "Docker Tool",
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM node:18",
|
||||
default_port: 3000,
|
||||
required_variables: [],
|
||||
});
|
||||
|
||||
expect(result.definition_type).toBe("dockerfile");
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
"/tool-types",
|
||||
expect.objectContaining({
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM node:18",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("creates tool type with readiness probe", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: "new-type",
|
||||
name: "probed-tool",
|
||||
readiness_probe: {
|
||||
command: "curl -f http://localhost:8080",
|
||||
timeout: 60,
|
||||
interval: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
mockedAxios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await createToolType({
|
||||
name: "probed-tool",
|
||||
display_name: "Probed Tool",
|
||||
compose_template: "version: '3.8'",
|
||||
default_port: 8080,
|
||||
required_variables: [],
|
||||
readiness_probe: {
|
||||
command: "curl -f http://localhost:8080",
|
||||
timeout: 60,
|
||||
interval: 3,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.readiness_probe).toEqual({
|
||||
command: "curl -f http://localhost:8080",
|
||||
timeout: 60,
|
||||
interval: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateToolType", () => {
|
||||
it("validates compose template", async () => {
|
||||
const mockResponse = {
|
||||
data: { valid: true, errors: [] },
|
||||
};
|
||||
mockedAxios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await validateToolType({
|
||||
definition_type: "compose",
|
||||
compose_template: "version: '3.8'",
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("returns validation errors", async () => {
|
||||
const mockResponse = {
|
||||
data: { valid: false, errors: ["Invalid YAML"] },
|
||||
};
|
||||
mockedAxios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await validateToolType({
|
||||
definition_type: "compose",
|
||||
compose_template: "invalid: yaml: [",
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain("Invalid YAML");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateToolType", () => {
|
||||
it("updates tool type with new fields", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
id: "type-1",
|
||||
name: "updated-tool",
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM python:3.11",
|
||||
},
|
||||
};
|
||||
mockedAxios.put.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await updateToolType("type-1", {
|
||||
definition_type: "dockerfile",
|
||||
dockerfile_template: "FROM python:3.11",
|
||||
});
|
||||
|
||||
expect(result.definition_type).toBe("dockerfile");
|
||||
expect(mockedAxios.put).toHaveBeenCalledWith(
|
||||
"/tool-types/type-1",
|
||||
expect.objectContaining({
|
||||
definition_type: "dockerfile",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteToolType", () => {
|
||||
it("deletes tool type", async () => {
|
||||
mockedAxios.delete.mockResolvedValue({ data: undefined });
|
||||
|
||||
await deleteToolType("type-1");
|
||||
|
||||
expect(mockedAxios.delete).toHaveBeenCalledWith("/tool-types/type-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user