Skip to main content
Glama
projects.controller.test.ts21.5 kB
import type { Project, ProjectDeleted, ProjectEmptied, } from "@lokalise/node-api"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // ControllerResponse type is used in function return type signatures import { ErrorType, McpError } from "../../shared/utils/error.util.js"; import { generators } from "../../test-utils/fixture-helpers/generators.js"; import { ProjectsMockBuilder } from "../../test-utils/mock-builders/projects.mock.js"; import type { CreateProjectToolArgsType, DeleteProjectToolArgsType, EmptyProjectToolArgsType, GetProjectDetailsToolArgsType, ListProjectsToolArgsType, UpdateProjectToolArgsType, } from "./projects.types.js"; // Mock the service module vi.mock("./projects.service.js"); import projectsController from "./projects.controller.js"; import projectsService from "./projects.service.js"; describe("ProjectsController", () => { // Get the mocked service const mockedService = vi.mocked(projectsService); beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); describe("listProjects", () => { it("should list projects with default parameters", async () => { // Arrange const mockBuilder = new ProjectsMockBuilder(); mockBuilder .withProject({ name: "Project 1", project_id: "123" }) .withProject({ name: "Project 2", project_id: "456" }); const mockProjects = mockBuilder.projects; mockedService.getProjects.mockResolvedValue(mockProjects); const args: ListProjectsToolArgsType = { includeStats: false, }; // Act const result = await projectsController.listProjects(args); // Assert expect(result).toHaveProperty("content"); expect(result.content).toContain("Project 1"); expect(result.content).toContain("Project 2"); expect(mockedService.getProjects).toHaveBeenCalledWith({ page: undefined, limit: undefined, }); }); it("should apply custom pagination parameters", async () => { // Arrange const mockProjects: Project[] = []; mockedService.getProjects.mockResolvedValue(mockProjects); const args: ListProjectsToolArgsType = { page: 2, limit: 50, includeStats: false, }; // Act await projectsController.listProjects(args); // Assert expect(mockedService.getProjects).toHaveBeenCalledWith({ page: 2, limit: 50, }); }); it("should validate page number", async () => { // Arrange const args: ListProjectsToolArgsType = { page: -1, // Invalid page includeStats: false, }; // Act & Assert await expect(projectsController.listProjects(args)).rejects.toThrow( McpError, ); await expect(projectsController.listProjects(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); it("should validate limit range", async () => { // Arrange const args: ListProjectsToolArgsType = { limit: 1001, // Exceeds maximum includeStats: false, }; // Act & Assert await expect(projectsController.listProjects(args)).rejects.toThrow( McpError, ); await expect(projectsController.listProjects(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); it("should handle includeStats parameter", async () => { // Arrange const mockBuilder = new ProjectsMockBuilder(); const mockProject = mockBuilder.withProject({ name: "Project with Stats", statistics: { keys_total: 100, progress_total: 75, team: 5, base_words: 500, qa_issues_total: 2, qa_issues: { not_reviewed: 0, unverified: 0, spelling_grammar: 0, inconsistent_placeholders: 0, inconsistent_html: 0, different_number_of_urls: 0, different_urls: 0, leading_whitespace: 0, trailing_whitespace: 0, different_number_of_email_address: 0, different_email_address: 0, different_brackets: 0, different_numbers: 0, double_space: 0, special_placeholder: 0, unbalanced_brackets: 0, }, languages: [], }, }).projects[0]; mockedService.getProjects.mockResolvedValue([mockProject]); const args: ListProjectsToolArgsType = { includeStats: true, }; // Act const result = await projectsController.listProjects(args); // Assert expect(result.content).toContain("Total Keys"); expect(result.content).toContain("75%"); }); it("should format empty project list", async () => { // Arrange mockedService.getProjects.mockResolvedValue([]); // Act const result = await projectsController.listProjects({ includeStats: false, }); // Assert expect(result.content).toContain("No projects found"); }); it("should generate pagination metadata", async () => { // Arrange const mockProjects = Array(50) .fill(null) .map((_, i) => ({ project_id: `id-${i}`, name: `Project ${i}`, })) as Project[]; mockedService.getProjects.mockResolvedValue(mockProjects); const args: ListProjectsToolArgsType = { page: 1, limit: 50, includeStats: false, }; // Act const result = await projectsController.listProjects(args); // Assert expect(result.content).toContain("50"); expect(result.content).toContain("Page 1"); }); it("should handle service errors", async () => { // Arrange mockedService.getProjects.mockRejectedValue( new McpError("Service unavailable", ErrorType.API_ERROR), ); // Act & Assert await expect( projectsController.listProjects({ includeStats: false }), ).rejects.toThrow("Service unavailable"); }); }); describe("getProjectDetails", () => { it("should get project details successfully", async () => { // Arrange const mockBuilder = new ProjectsMockBuilder(); const mockProject = mockBuilder.withProject({ project_id: "test-123", name: "Test Project", description: "Test Description", statistics: { keys_total: 100, progress_total: 75, team: 5, base_words: 500, qa_issues_total: 2, qa_issues: { not_reviewed: 0, unverified: 0, spelling_grammar: 0, inconsistent_placeholders: 0, inconsistent_html: 0, different_number_of_urls: 0, different_urls: 0, leading_whitespace: 0, trailing_whitespace: 0, different_number_of_email_address: 0, different_email_address: 0, different_brackets: 0, different_numbers: 0, double_space: 0, special_placeholder: 0, unbalanced_brackets: 0, }, languages: [], }, }).projects[0]; mockedService.getProjectDetails.mockResolvedValue(mockProject); const args: GetProjectDetailsToolArgsType = { projectId: "test-123", includeLanguages: false, includeKeysSummary: false, }; // Act const result = await projectsController.getProjectDetails(args); // Assert expect(result.content).toContain("Test Project"); expect(result.content).toContain("Test Description"); expect(mockedService.getProjectDetails).toHaveBeenCalledWith("test-123"); }); it("should validate project ID format", async () => { // Arrange const args: GetProjectDetailsToolArgsType = { projectId: "", // Empty ID includeLanguages: false, includeKeysSummary: false, }; // Act & Assert await expect(projectsController.getProjectDetails(args)).rejects.toThrow( McpError, ); await expect(projectsController.getProjectDetails(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); it("should handle includeLanguages option", async () => { // Arrange const mockBuilder = new ProjectsMockBuilder(); const mockProject = mockBuilder.withProject({ project_id: "test-123", name: "Test Project", statistics: { progress_total: 75, keys_total: 100, base_words: 500, team: 5, qa_issues_total: 0, qa_issues: {} as Project["statistics"]["qa_issues"], languages: [ { language_id: 1, language_iso: "en", progress: 100, words_to_do: 0, }, { language_id: 2, language_iso: "de", progress: 50, words_to_do: 250, }, ], }, }).projects[0]; mockedService.getProjectDetails.mockResolvedValue(mockProject); const args: GetProjectDetailsToolArgsType = { projectId: "test-123", includeLanguages: true, includeKeysSummary: false, }; // Act const result = await projectsController.getProjectDetails(args); // Assert expect(result.content).toContain("en"); expect(result.content).toContain("de"); expect(result.content).toContain("100%"); expect(result.content).toContain("50%"); }); it("should handle includeKeysSummary option", async () => { // Arrange const mockBuilder = new ProjectsMockBuilder(); const mockProject = mockBuilder.withProject({ project_id: "test-123", name: "Test Project", statistics: { progress_total: 66, keys_total: 150, base_words: 750, team: 3, qa_issues_total: 0, qa_issues: {} as Project["statistics"]["qa_issues"], languages: [], }, }).projects[0]; mockedService.getProjectDetails.mockResolvedValue(mockProject); const args: GetProjectDetailsToolArgsType = { projectId: "test-123", includeLanguages: false, includeKeysSummary: true, }; // Act const result = await projectsController.getProjectDetails(args); // Assert expect(result.content).toContain("150"); }); it("should handle project not found", async () => { // Arrange mockedService.getProjectDetails.mockRejectedValue( new McpError("Project not found", ErrorType.NOT_FOUND), ); const args: GetProjectDetailsToolArgsType = { projectId: "non-existent", includeLanguages: false, includeKeysSummary: false, }; // Act & Assert await expect(projectsController.getProjectDetails(args)).rejects.toThrow( "Lokalise Project projectId: non-existent not found", ); }); }); describe("createProject", () => { it("should create project with minimal data", async () => { // Arrange const mockProject = { project_id: generators.id.project(1), name: "New Project", created_at: generators.timestamp().formatted, } as Project; mockedService.createProject.mockResolvedValue(mockProject); const args: CreateProjectToolArgsType = { name: "New Project", base_lang_iso: "en", }; // Act const result = await projectsController.createProject(args); // Assert expect(result.content).toContain("New Project"); expect(result.content).toContain("Project Created Successfully"); expect(mockedService.createProject).toHaveBeenCalledWith({ name: "New Project", description: undefined, base_lang_iso: "en", // Controller defaults to 'en' languages: undefined, }); }); it("should create project with full data", async () => { // Arrange const mockProject = { project_id: generators.id.project(2), name: "Full Project", description: "Complete project", base_language_iso: "de", } as Project; mockedService.createProject.mockResolvedValue(mockProject); const args: CreateProjectToolArgsType = { name: "Full Project", description: "Complete project", base_lang_iso: "de", languages: [{ lang_iso: "de" }, { lang_iso: "en" }, { lang_iso: "fr" }], }; // Act const result = await projectsController.createProject(args); // Assert expect(result.content).toContain("Full Project"); expect(mockedService.createProject).toHaveBeenCalledWith({ name: "Full Project", description: "Complete project", base_lang_iso: "de", languages: [{ lang_iso: "de" }, { lang_iso: "en" }, { lang_iso: "fr" }], }); }); it("should validate project name", async () => { // Arrange const args: CreateProjectToolArgsType = { name: "", // Empty name base_lang_iso: "en", }; // Act & Assert await expect(projectsController.createProject(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); it("should validate project name length", async () => { // Arrange const args: CreateProjectToolArgsType = { name: "x".repeat(256), // Too long base_lang_iso: "en", }; // Act & Assert await expect(projectsController.createProject(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); it("should validate base language ISO", async () => { // Arrange mockedService.createProject.mockRejectedValue( new McpError( "VALIDATION_ERROR: Invalid language ISO code", ErrorType.VALIDATION_ERROR, ), ); const args: CreateProjectToolArgsType = { name: "Test Project", base_lang_iso: "invalid", // Invalid ISO code }; // Act & Assert await expect(projectsController.createProject(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); it("should handle duplicate project name error", async () => { // Arrange mockedService.createProject.mockRejectedValue( new McpError("Project name already exists", ErrorType.API_ERROR), ); const args: CreateProjectToolArgsType = { name: "Existing Project", base_lang_iso: "en", }; // Act & Assert await expect(projectsController.createProject(args)).rejects.toThrow( "Project name already exists", ); }); }); describe("updateProject", () => { it("should update project name", async () => { // Arrange const mockProject = { project_id: "test-123", name: "Updated Name", } as Project; mockedService.updateProject.mockResolvedValue(mockProject); const args: UpdateProjectToolArgsType = { projectId: "test-123", projectData: { name: "Updated Name", }, }; // Act const result = await projectsController.updateProject(args); // Assert expect(result.content).toContain("Updated Name"); expect(result.content).toContain("Project Updated Successfully"); expect(mockedService.updateProject).toHaveBeenCalledWith("test-123", { name: "Updated Name", description: undefined, }); }); it("should update project description", async () => { // Arrange const mockProject = { project_id: "test-123", description: "New description", } as Project; mockedService.updateProject.mockResolvedValue(mockProject); const args: UpdateProjectToolArgsType = { projectId: "test-123", projectData: { description: "New description", }, }; // Act const result = await projectsController.updateProject(args); // Assert expect(result.content).toContain("New description"); expect(mockedService.updateProject).toHaveBeenCalledWith("test-123", { name: undefined, description: "New description", }); }); it("should validate at least one update field", async () => { // Arrange const args: UpdateProjectToolArgsType = { projectId: "test-123", projectData: { // No update fields provided }, }; // Act & Assert await expect(projectsController.updateProject(args)).rejects.toThrow( "VALIDATION_ERROR", ); await expect(projectsController.updateProject(args)).rejects.toThrow( "At least one field", ); }); it("should handle partial updates", async () => { // Arrange const mockProject = { project_id: "test-123", name: "Partially Updated", description: "Original description", } as Project; mockedService.updateProject.mockResolvedValue(mockProject); const args: UpdateProjectToolArgsType = { projectId: "test-123", projectData: { name: "Partially Updated", }, }; // Act const result = await projectsController.updateProject(args); // Assert expect(result.content).toContain("Partially Updated"); expect(mockedService.updateProject).toHaveBeenCalledWith("test-123", { name: "Partially Updated", description: undefined, }); }); }); describe("deleteProject", () => { it("should delete project successfully", async () => { // Arrange const mockResult: ProjectDeleted = { project_deleted: true, project_id: "test-123", }; mockedService.deleteProject.mockResolvedValue(mockResult); const args: DeleteProjectToolArgsType = { projectId: "test-123", }; // Act const result = await projectsController.deleteProject(args); // Assert expect(result.content).toContain("Project Deleted Successfully"); expect(mockedService.deleteProject).toHaveBeenCalledWith("test-123"); }); it("should handle deletion failure", async () => { // Arrange const mockResult: ProjectDeleted = { project_deleted: false, project_id: "test-123", }; mockedService.deleteProject.mockResolvedValue(mockResult); const args: DeleteProjectToolArgsType = { projectId: "test-123", }; // Act const result = await projectsController.deleteProject(args); // Assert expect(result.content).toContain("Project Deleted Successfully"); }); it("should validate project ID", async () => { // Arrange const args: DeleteProjectToolArgsType = { projectId: "", }; // Act & Assert await expect(projectsController.deleteProject(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); }); describe("emptyProject", () => { it("should empty project successfully", async () => { // Arrange const mockResult: ProjectEmptied = { project_id: "test-123", keys_deleted: true, }; mockedService.emptyProject.mockResolvedValue(mockResult); const args: EmptyProjectToolArgsType = { projectId: "test-123", }; // Act const result = await projectsController.emptyProject(args); // Assert expect(result.content).toContain("Project Emptied Successfully"); expect(mockedService.emptyProject).toHaveBeenCalledWith("test-123"); }); it("should handle emptying already empty project", async () => { // Arrange const mockResult: ProjectEmptied = { project_id: "test-123", keys_deleted: true, }; mockedService.emptyProject.mockResolvedValue(mockResult); const args: EmptyProjectToolArgsType = { projectId: "test-123", }; // Act const result = await projectsController.emptyProject(args); // Assert expect(result.content).toContain("Project Emptied Successfully"); }); it("should handle large number of keys", async () => { // Arrange const mockResult: ProjectEmptied = { project_id: "test-123", keys_deleted: true, }; mockedService.emptyProject.mockResolvedValue(mockResult); const args: EmptyProjectToolArgsType = { projectId: "test-123", }; // Act const result = await projectsController.emptyProject(args); // Assert expect(result.content).toContain("Project Emptied Successfully"); }); it("should validate project ID", async () => { // Arrange const args: EmptyProjectToolArgsType = { projectId: "", }; // Act & Assert await expect(projectsController.emptyProject(args)).rejects.toThrow( "VALIDATION_ERROR", ); }); }); describe("Error Handling", () => { it("should transform service errors to controller format", async () => { // Arrange mockedService.getProjects.mockRejectedValue(new Error("Network error")); // Act & Assert await expect( projectsController.listProjects({ includeStats: false }), ).rejects.toThrow(McpError); }); it("should preserve error context", async () => { // Arrange const error = new McpError( "Rate limited", ErrorType.RATE_LIMIT_EXCEEDED, 429, ); mockedService.getProjects.mockRejectedValue(error); // Act & Assert try { await projectsController.listProjects({ includeStats: false }); } catch (err) { expect(err).toBeInstanceOf(McpError); const mcpErr = err as McpError; expect(mcpErr.statusCode).toBe(429); } }); it("should add controller context to errors", async () => { // Arrange mockedService.getProjectDetails.mockRejectedValue( new Error("Database connection failed"), ); // Act & Assert try { await projectsController.getProjectDetails({ projectId: "test-123", includeLanguages: false, includeKeysSummary: false, }); } catch (err) { expect(err).toBeInstanceOf(McpError); const mcpErr = err as McpError; expect(mcpErr.message).toContain("Database connection failed"); } }); }); describe("Response Formatting", () => { it("should format response as ControllerResponse", async () => { // Arrange const mockProjects = [{ name: "Test" }] as Project[]; mockedService.getProjects.mockResolvedValue(mockProjects); // Act const result = await projectsController.listProjects({ includeStats: false, }); // Assert expect(result).toHaveProperty("content"); expect(typeof result.content).toBe("string"); }); it("should include markdown formatting in content", async () => { // Arrange const mockProject = { project_id: "test-123", name: "Test Project", description: "Test Description", } as Project; mockedService.getProjectDetails.mockResolvedValue(mockProject); // Act const result = await projectsController.getProjectDetails({ projectId: "test-123", includeLanguages: false, includeKeysSummary: false, }); // Assert expect(result.content).toContain("#"); // Markdown header expect(result.content).toContain("**"); // Bold text expect(result.content).toContain("-"); // List items }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AbdallahAHO/lokalise-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server