Skip to main content
Glama
scaffold-tests.ts•20.6 kB
#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import chalk from "chalk"; import { program } from "commander"; import inquirer from "inquirer"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); interface TestConfig { domain: string; layer: "service" | "controller" | "tool" | "resource" | "cli" | "formatter"; includePerformance: boolean; includeMocks: boolean; includeIntegration: boolean; } class TestScaffolder { private config: TestConfig = { domain: "", layer: "service", includePerformance: false, includeMocks: true, includeIntegration: false, }; async run(options?: Partial<TestConfig>) { if (options && Object.keys(options).length > 0) { // CLI mode this.config = { ...this.config, ...options }; await this.generateTests(); } else { // Interactive mode await this.runInteractive(); } } private async runInteractive() { console.log(chalk.cyan.bold("\nđź§Ş Test Scaffolding Wizard\n")); const answers = await inquirer.prompt([ { type: "input", name: "domain", message: "Domain name (e.g., projects, keys):", validate: (input) => { if (!input) return "Domain name is required"; const domainPath = path.join( __dirname, "..", "src", "domains", input, ); if (!fs.existsSync(domainPath)) { return `Domain '${input}' does not exist`; } return true; }, }, { type: "list", name: "layer", message: "Which layer to test?", choices: [ { name: "Service (API calls)", value: "service" }, { name: "Controller (business logic)", value: "controller" }, { name: "Tool (MCP tools)", value: "tool" }, { name: "Resource (MCP resources)", value: "resource" }, { name: "CLI (command line)", value: "cli" }, { name: "Formatter (output formatting)", value: "formatter" }, ], }, { type: "confirm", name: "includeMocks", message: "Include mock setup?", default: true, }, { type: "confirm", name: "includePerformance", message: "Include performance tests?", default: false, }, { type: "confirm", name: "includeIntegration", message: "Include integration tests?", default: false, }, ]); this.config = answers; await this.generateTests(); } private async generateTests() { const { domain, layer } = this.config; const testFilePath = path.join( __dirname, "..", "src", "domains", domain, `${domain}.${layer}.test.ts`, ); if (fs.existsSync(testFilePath)) { const { overwrite } = await inquirer.prompt([ { type: "confirm", name: "overwrite", message: "Test file already exists. Overwrite?", default: false, }, ]); if (!overwrite) { console.log(chalk.yellow("Test generation cancelled.")); return; } } const template = this.generateTemplate(); fs.writeFileSync(testFilePath, template); console.log(chalk.green(`\nâś… Test file created: ${testFilePath}`)); console.log(chalk.gray("\nNext steps:")); console.log(chalk.gray("1. Review and customize the generated tests")); console.log( chalk.gray( `2. Run tests: npm test -- src/domains/${domain}/${domain}.${layer}.test.ts`, ), ); console.log(chalk.gray("3. Check coverage: npm run test:coverage")); } private generateTemplate(): string { const { domain, layer, includePerformance, includeMocks, includeIntegration, } = this.config; const domainCapitalized = domain.charAt(0).toUpperCase() + domain.slice(1); const layerCapitalized = layer.charAt(0).toUpperCase() + layer.slice(1); let template = ""; // Generate based on layer type switch (layer) { case "service": template = this.generateServiceTemplate( domain, domainCapitalized, includeMocks, includePerformance, includeIntegration, ); break; case "controller": template = this.generateControllerTemplate( domain, domainCapitalized, includeMocks, ); break; case "tool": template = this.generateToolTemplate(domain, domainCapitalized); break; case "resource": template = this.generateResourceTemplate(domain, domainCapitalized); break; case "cli": template = this.generateCliTemplate(domain, domainCapitalized); break; case "formatter": template = this.generateFormatterTemplate(domain, domainCapitalized); break; default: template = this.generateGenericTemplate( domain, domainCapitalized, layerCapitalized, ); } return template; } private generateServiceTemplate( domain: string, domainCap: string, includeMocks: boolean, includePerformance: boolean, includeIntegration: boolean, ): string { return `import { describe, it, expect, beforeEach, jest } from "@jest/globals"; import { ${domainCap}Service } from "./${domain}.service.js"; ${ includeMocks ? `import { createMockLokaliseApi } from "../../test-utils/mock-factory.js"; import { ${domainCap}MockBuilder } from "../../test-utils/mock-builders/${domain}.mock.js"; import { ApiErrorSimulator } from "../../test-utils/error-simulator.js";` : "" } ${ includePerformance ? `import { measurePerformance, testBulkPerformance } from "../../test-utils/performance.util.js";` : "" } import type { LokaliseApi } from "@lokalise/node-api"; describe("${domainCap}Service", () => { let service: ${domainCap}Service; ${includeMocks ? "let mockApi: ReturnType<typeof createMockLokaliseApi>;" : ""} beforeEach(() => { ${ includeMocks ? `mockApi = createMockLokaliseApi(); service = new ${domainCap}Service(); (service as unknown).lokaliseApi = mockApi;` : `service = new ${domainCap}Service();` } }); describe("list${domainCap}", () => { it("should successfully list ${domain} with pagination", async () => { ${ includeMocks ? `// Arrange const mockResponse = new ${domainCap}MockBuilder() .withPagination(1, 10) .build(); mockApi.${domain}().list.mockResolvedValue(mockResponse); // Act const result = await service.list${domainCap}({ page: 1, limit: 10 }); // Assert expect(result).toBeDefined(); expect(mockApi.${domain}().list).toHaveBeenCalledWith({ page: 1, limit: 10 });` : "// TODO: Implement test" } }); it("should handle empty results", async () => { ${ includeMocks ? `// Arrange const mockResponse = new ${domainCap}MockBuilder() .withPagination(1, 10) .build(); mockResponse.items = []; mockApi.${domain}().list.mockResolvedValue(mockResponse); // Act const result = await service.list${domainCap}({ page: 1, limit: 10 }); // Assert expect(result.items).toHaveLength(0);` : "// TODO: Implement test" } }); it("should handle API errors gracefully", async () => { ${ includeMocks ? `// Arrange mockApi.${domain}().list.mockRejectedValue( ApiErrorSimulator.unauthorized() ); // Act & Assert await expect(service.list${domainCap}({})) .rejects.toThrow("Invalid API token");` : "// TODO: Implement test" } }); ${ includePerformance ? `it("should handle large datasets efficiently", async () => { const { metrics } = await measurePerformance( async () => { const mockResponse = new ${domainCap}MockBuilder(); for (let i = 0; i < 1000; i++) { mockResponse.with${domainCap.slice(0, -1)}({ /* data */ }); } mockApi.${domain}().list.mockResolvedValue(mockResponse.build()); return await service.list${domainCap}({ limit: 1000 }); }, "List 1000 ${domain}" ); expect(metrics.duration).toBeLessThan(1000); // Should complete within 1 second expect(metrics.memoryDelta).toBeLessThan(50 * 1024 * 1024); // Less than 50MB });` : "" } }); describe("get${domainCap.slice(0, -1)}", () => { it("should fetch a single ${domain.slice(0, -1)} by ID", async () => { ${ includeMocks ? `// Arrange const mock${domainCap.slice(0, -1)} = { /* mock data */ }; mockApi.${domain}().get.mockResolvedValue(mock${domainCap.slice(0, -1)}); // Act const result = await service.get${domainCap.slice(0, -1)}("test_id"); // Assert expect(result).toEqual(mock${domainCap.slice(0, -1)}); expect(mockApi.${domain}().get).toHaveBeenCalledWith("test_id");` : "// TODO: Implement test" } }); it("should handle not found errors", async () => { ${ includeMocks ? `// Arrange mockApi.${domain}().get.mockRejectedValue( ApiErrorSimulator.notFound("${domainCap.slice(0, -1)}") ); // Act & Assert await expect(service.get${domainCap.slice(0, -1)}("invalid_id")) .rejects.toThrow("not found");` : "// TODO: Implement test" } }); }); ${ includeIntegration ? `describe("Integration Tests", () => { it("should handle full CRUD workflow", async () => { // TODO: Implement integration test // 1. Create // 2. Read // 3. Update // 4. Delete }); });` : "" } });`; } private generateControllerTemplate( domain: string, domainCap: string, _includeMocks: boolean, ): string { return `import { describe, it, expect, beforeEach, jest } from "@jest/globals"; import { ${domain}Controller } from "./${domain}.controller.js"; import { ${domainCap}Service } from "./${domain}.service.js"; import { McpError } from "../../shared/utils/error.util.js"; jest.mock("./${domain}.service.js"); describe("${domainCap}Controller", () => { let controller: typeof ${domain}Controller; let mockService: jest.Mocked<${domainCap}Service>; beforeEach(() => { jest.clearAllMocks(); mockService = new ${domainCap}Service() as jest.Mocked<${domainCap}Service>; controller = ${domain}Controller; }); describe("list${domainCap}", () => { it("should validate input parameters", async () => { // Test with invalid parameters await expect(controller.list${domainCap}({ page: -1, limit: 0 })).rejects.toThrow(McpError); }); it("should format response correctly", async () => { // Arrange const mockData = { items: [], totalResults: 0 }; mockService.list${domainCap}.mockResolvedValue(mockData); // Act const result = await controller.list${domainCap}({}); // Assert expect(result).toHaveProperty("content"); expect(result).toHaveProperty("data"); expect(result.data).toEqual(mockData); }); it("should handle service errors", async () => { // Arrange mockService.list${domainCap}.mockRejectedValue( new Error("Service error") ); // Act & Assert await expect(controller.list${domainCap}({})) .rejects.toThrow("Service error"); }); }); });`; } private generateToolTemplate(domain: string, domainCap: string): string { return `import { describe, it, expect, beforeEach, jest } from "@jest/globals"; import { ${domain}Tool } from "./${domain}.tool.js"; import { ${domain}Controller } from "./${domain}.controller.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; jest.mock("./${domain}.controller.js"); describe("${domainCap}Tool", () => { let mockServer: unknown; beforeEach(() => { jest.clearAllMocks(); mockServer = { tool: jest.fn(), }; }); describe("registerTools", () => { it("should register all ${domain} tools", () => { // Act ${domain}Tool.registerTools(mockServer); // Assert expect(mockServer.tool).toHaveBeenCalled(); const calls = mockServer.tool.mock.calls; // Verify tool names const toolNames = calls.map((call: unknown) => call[0]); expect(toolNames).toContain("lokalise_list_${domain}"); expect(toolNames).toContain("lokalise_get_${domain.slice(0, -1)}"); }); it("should handle tool execution", async () => { // Register tools ${domain}Tool.registerTools(mockServer); // Get the list handler const listHandler = mockServer.tool.mock.calls .find((call: unknown) => call[0] === "lokalise_list_${domain}")[3]; // Mock controller response (${domain}Controller.list${domainCap} as jest.Mock).mockResolvedValue({ content: "Mocked response", data: { items: [] } }); // Execute handler const result = await listHandler({ page: 1, limit: 10 }); // Assert expect(result).toHaveProperty("content"); expect(${domain}Controller.list${domainCap}).toHaveBeenCalledWith({ page: 1, limit: 10 }); }); }); describe("getMeta", () => { it("should return domain metadata", () => { const meta = ${domain}Tool.getMeta(); expect(meta).toHaveProperty("name", "${domain}"); expect(meta).toHaveProperty("description"); expect(meta).toHaveProperty("toolsCount"); expect(meta.toolsCount).toBeGreaterThan(0); }); }); });`; } private generateResourceTemplate(domain: string, domainCap: string): string { return `import { describe, it, expect, beforeEach, jest } from "@jest/globals"; import { ${domain}Resource } from "./${domain}.resource.js"; import { ${domain}Controller } from "./${domain}.controller.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; jest.mock("./${domain}.controller.js"); describe("${domainCap}Resource", () => { let mockServer: unknown; beforeEach(() => { jest.clearAllMocks(); mockServer = { resource: jest.fn(), }; }); describe("registerResources", () => { it("should register all ${domain} resources", () => { // Act ${domain}Resource.registerResources(mockServer); // Assert expect(mockServer.resource).toHaveBeenCalled(); const calls = mockServer.resource.mock.calls; // Verify resource names const resourceNames = calls.map((call: unknown) => call[0]); expect(resourceNames.length).toBeGreaterThan(0); }); it("should handle resource URI parsing", async () => { // Register resources ${domain}Resource.registerResources(mockServer); // Get the first resource handler const handler = mockServer.resource.mock.calls[0][2]; // Test URI parsing const uri = "lokalise://${domain}/test_project?page=1&limit=10"; // Mock controller response (${domain}Controller.list${domainCap} as jest.Mock).mockResolvedValue({ content: "Resource content", data: { items: [] } }); // Execute handler const result = await handler({ uri }); // Assert expect(result).toHaveProperty("text"); }); }); describe("getMeta", () => { it("should return resource metadata", () => { const meta = ${domain}Resource.getMeta(); expect(meta).toHaveProperty("name", "${domain}"); expect(meta).toHaveProperty("description"); expect(meta).toHaveProperty("resourcesCount"); expect(meta.resourcesCount).toBeGreaterThan(0); }); }); });`; } private generateCliTemplate(domain: string, domainCap: string): string { return `import { describe, it, expect, beforeEach, jest } from "@jest/globals"; import { ${domain}Cli } from "./${domain}.cli.js"; import { ${domain}Controller } from "./${domain}.controller.js"; import { Command } from "commander"; jest.mock("./${domain}.controller.js"); describe("${domainCap}Cli", () => { let program: Command; beforeEach(() => { jest.clearAllMocks(); program = new Command(); program.exitOverride(); // Prevent process.exit in tests }); describe("register", () => { it("should register all ${domain} CLI commands", () => { // Act ${domain}Cli.register(program); // Assert const commands = program.commands.map(cmd => cmd.name()); expect(commands).toContain("list-${domain}"); expect(commands.length).toBeGreaterThan(0); }); it("should execute list command", async () => { // Register commands ${domain}Cli.register(program); // Mock controller (${domain}Controller.list${domainCap} as jest.Mock).mockResolvedValue({ content: "CLI output", data: { items: [] } }); // Spy on console.log const consoleSpy = jest.spyOn(console, "log").mockImplementation(); // Execute command await program.parseAsync(["node", "test", "list-${domain}", "--page", "1"]); // Assert expect(${domain}Controller.list${domainCap}).toHaveBeenCalled(); expect(consoleSpy).toHaveBeenCalledWith("CLI output"); consoleSpy.mockRestore(); }); }); describe("getMeta", () => { it("should return CLI metadata", () => { const meta = ${domain}Cli.getMeta(); expect(meta).toHaveProperty("name", "${domain}"); expect(meta).toHaveProperty("description"); expect(meta).toHaveProperty("cliCommandsCount"); expect(meta.cliCommandsCount).toBeGreaterThan(0); }); }); });`; } private generateFormatterTemplate(domain: string, domainCap: string): string { return `import { describe, it, expect } from "@jest/globals"; import * as formatter from "./${domain}.formatter.js"; import { generators } from "../../test-utils/fixture-helpers/generators.js"; describe("${domainCap}Formatter", () => { describe("format${domainCap}List", () => { it("should format empty list", () => { const result = formatter.format${domainCap}List([]); expect(result).toContain("No ${domain} found"); }); it("should format single item", () => { const item = { // Add mock ${domain.slice(0, -1)} properties id: "test_id", name: "Test ${domainCap.slice(0, -1)}" }; const result = formatter.format${domainCap}List([item]); expect(result).toContain("Test ${domainCap.slice(0, -1)}"); expect(result).toContain("test_id"); }); it("should format multiple items", () => { const items = Array.from({ length: 5 }, (_, i) => ({ id: \`test_id_\${i}\`, name: \`${domainCap.slice(0, -1)} \${i}\` })); const result = formatter.format${domainCap}List(items); expect(result).toContain("${domainCap} (5)"); items.forEach(item => { expect(result).toContain(item.name); }); }); it("should handle special characters", () => { const item = { id: "test_id", name: "Test & ${domainCap.slice(0, -1)} <with> 'special' \\"chars\\"" }; const result = formatter.format${domainCap}List([item]); expect(result).toBeDefined(); expect(result).not.toContain("undefined"); }); }); describe("format${domainCap.slice(0, -1)}Detail", () => { it("should format detailed view", () => { const item = { id: "test_id", name: "Test ${domainCap.slice(0, -1)}", created_at: generators.timestamp().formatted, // Add more properties }; const result = formatter.format${domainCap.slice(0, -1)}Detail(item); expect(result).toContain("test_id"); expect(result).toContain("Test ${domainCap.slice(0, -1)}"); expect(result).toContain("Created"); }); it("should handle null/undefined values", () => { const item = { id: "test_id", name: null, description: undefined }; const result = formatter.format${domainCap.slice(0, -1)}Detail(item as unknown); expect(result).toBeDefined(); expect(result).not.toContain("null"); expect(result).not.toContain("undefined"); }); }); });`; } private generateGenericTemplate( domain: string, domainCap: string, layerCap: string, ): string { return `import { describe, it, expect, beforeEach } from "@jest/globals"; import { ${domainCap}${layerCap} } from "./${domain}.${this.config.layer}.js"; describe("${domainCap}${layerCap}", () => { let instance: ${domainCap}${layerCap}; beforeEach(() => { instance = new ${domainCap}${layerCap}(); }); describe("initialization", () => { it("should create instance successfully", () => { expect(instance).toBeDefined(); }); }); // Add more test cases based on the specific layer functionality });`; } } // CLI setup program .name("scaffold-tests") .description("Generate test files for lokalise-mcp domains") .option("-d, --domain <domain>", "Domain name (e.g., projects, keys)") .option( "-l, --layer <layer>", "Layer to test (service, controller, tool, resource, cli, formatter)", ) .option("--no-mocks", "Skip mock setup") .option("--performance", "Include performance tests") .option("--integration", "Include integration tests") .action(async (options) => { const scaffolder = new TestScaffolder(); const config: Partial<TestConfig> = {}; if (options.domain) config.domain = options.domain; if (options.layer) config.layer = options.layer; if (options.mocks === false) config.includeMocks = false; if (options.performance) config.includePerformance = true; if (options.integration) config.includeIntegration = true; await scaffolder.run(Object.keys(config).length > 0 ? config : undefined); }); program.parse(); export { TestScaffolder };

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