Skip to main content
Glama

JIRA MCP Server

get-projects.handler.test.ts25.9 kB
/** * Get Projects Handler Unit Tests * Comprehensive unit tests for JIRA get projects MCP tool handler */ import { type Mock, afterEach, beforeEach, describe, expect, mock, test, } from "bun:test"; import type { McpResponse } from "@core/responses/mcp-response.types"; import { JiraApiError, JiraPermissionError, } from "@features/jira/client/errors"; import { GetProjectsHandler } from "@features/jira/projects/handlers/get-projects.handler"; import type { Project } from "@features/jira/projects/models"; import type { GetProjectsUseCase } from "@features/jira/projects/use-cases/get-projects.use-case"; import type { GetProjectParams, GetProjectsParams, ProjectParamsValidator, } from "@features/jira/projects/validators/project-params.validator"; import { mockFactory } from "@test/mocks/jira-mock-factory"; import { jiraApiMocks } from "@test/utils/mock-helpers"; import { setupTests } from "@test/utils/test-setup"; // Setup test environment setupTests(); describe("GetProjectsHandler", () => { let handler: GetProjectsHandler; let mockUseCase: GetProjectsUseCase; let mockValidator: ProjectParamsValidator; beforeEach(() => { // Create mock use case with proper typing mockUseCase = { execute: mock() as Mock< (params: GetProjectsParams) => Promise<Project[]> >, }; mockValidator = { validateGetProjectsParams: mock((params) => params) as Mock< (params: GetProjectsParams) => GetProjectsParams >, validateGetProjectParams: mock((params) => params) as Mock< (params: GetProjectParams) => GetProjectParams >, }; // Create handler with mock use case handler = new GetProjectsHandler(mockUseCase, mockValidator); }); afterEach(() => { jiraApiMocks.clearMocks(); mock.restore(); }); describe("basic project retrieval", () => { test("should retrieve all projects successfully", async () => { const mockProjects = [ mockFactory.createMockProject({ key: "PROJ1", name: "Project One" }), mockFactory.createMockProject({ key: "PROJ2", name: "Project Two" }), mockFactory.createMockProject({ key: "PROJ3", name: "Project Three" }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle({})) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("PROJ1"); expect(result.data).toContain("PROJ2"); expect(result.data).toContain("PROJ3"); expect(result.data).toContain("Project One"); expect(result.data).toContain("Project Two"); expect(result.data).toContain("Project Three"); // Verify use case was called expect(mockUseCase.execute).toHaveBeenCalled(); }); test("should handle empty project list", async () => { // Setup mock use case to return empty array ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue([]); const result = (await handler.handle({})) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("No Projects Found"); // Verify use case was called expect(mockUseCase.execute).toHaveBeenCalled(); }); test("should retrieve projects with basic parameters", async () => { const params: GetProjectsParams = { maxResults: 5, startAt: 0, }; const mockProjects = [ mockFactory.createMockProject({ key: "TEST1", name: "Test Project 1" }), mockFactory.createMockProject({ key: "TEST2", name: "Test Project 2" }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("TEST1"); expect(result.data).toContain("TEST2"); // Verify use case was called with correct parameters expect(mockUseCase.execute).toHaveBeenCalledWith( expect.objectContaining({ maxResults: 5, }), ); }); }); describe("filtering and search", () => { test("should filter projects by search query", async () => { const params: GetProjectsParams = { searchQuery: "web", maxResults: 50, startAt: 0, }; const mockProjects = [ mockFactory.createMockProject({ key: "WEB1", name: "Web Application Project", projectTypeKey: "software", }), mockFactory.createMockProject({ key: "WEB2", name: "Website Redesign", projectTypeKey: "software", }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("WEB1"); expect(result.data).toContain("WEB2"); expect(result.data).toContain("Web Application Project"); expect(result.data).toContain("Website Redesign"); expect(result.data).toContain("# 📋 JIRA Projects"); }); test("should filter projects by type", async () => { const params: GetProjectsParams = { typeKey: "software", maxResults: 50, startAt: 0, }; const mockProjects = [ mockFactory.createMockProject({ key: "SOFT1", name: "Software Project 1", projectTypeKey: "software", }), mockFactory.createMockProject({ key: "SOFT2", name: "Software Project 2", projectTypeKey: "software", }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("SOFT1"); expect(result.data).toContain("SOFT2"); expect(result.data).toContain("software"); }); test("should filter projects by category", async () => { const params: GetProjectsParams = { categoryId: 10001, maxResults: 50, startAt: 0, }; const mockProjects = [ mockFactory.createMockProject({ key: "CAT1", name: "Category Project 1", projectCategory: { id: "10001", name: "Development", description: "Development projects", }, }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("CAT1"); expect(result.data).toContain("Development"); }); test("should retrieve recent projects", async () => { const params: GetProjectsParams = { recent: 5, maxResults: 50, startAt: 0, }; const mockProjects = [ mockFactory.createMockProject({ key: "REC1", name: "Recent Project 1", }), mockFactory.createMockProject({ key: "REC2", name: "Recent Project 2", }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("REC1"); expect(result.data).toContain("REC2"); expect(result.data).toContain("recently accessed projects"); }); test("should combine multiple filters", async () => { const params: GetProjectsParams = { searchQuery: "api", typeKey: "software", maxResults: 10, startAt: 0, orderBy: "name", }; const mockProjects = [ mockFactory.createMockProject({ key: "API1", name: "API Gateway Project", projectTypeKey: "software", }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("API1"); expect(result.data).toContain("API Gateway Project"); expect(result.data).toContain("# 📋 JIRA Projects"); expect(result.data).toContain("software"); }); }); describe("ordering and pagination", () => { test("should order projects by name", async () => { const params: GetProjectsParams = { orderBy: "name", maxResults: 50, startAt: 0, }; const mockProjects = [ mockFactory.createMockProject({ key: "ALPHA", name: "Alpha Project" }), mockFactory.createMockProject({ key: "BETA", name: "Beta Project" }), mockFactory.createMockProject({ key: "GAMMA", name: "Gamma Project" }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("ALPHA"); expect(result.data).toContain("BETA"); expect(result.data).toContain("GAMMA"); expect(result.data).toContain("# 📋 JIRA Projects"); }); test("should order projects by key", async () => { const params: GetProjectsParams = { orderBy: "key", maxResults: 50, startAt: 0, }; const mockProjects = [ mockFactory.createMockProject({ key: "AAA", name: "Project AAA" }), mockFactory.createMockProject({ key: "BBB", name: "Project BBB" }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("AAA"); expect(result.data).toContain("BBB"); expect(result.data).toContain("# 📋 JIRA Projects"); }); test("should handle pagination with startAt", async () => { const params: GetProjectsParams = { maxResults: 2, startAt: 5, }; const mockProjects = [ mockFactory.createMockProject({ key: "PAGE1", name: "Page Project 1" }), mockFactory.createMockProject({ key: "PAGE2", name: "Page Project 2" }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("PAGE1"); expect(result.data).toContain("PAGE2"); }); test("should indicate when more results are available", async () => { const params: GetProjectsParams = { maxResults: 2, startAt: 0, }; // Return exactly maxResults to indicate there might be more const mockProjects = [ mockFactory.createMockProject({ key: "MORE1", name: "More Project 1" }), mockFactory.createMockProject({ key: "MORE2", name: "More Project 2" }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("MORE1"); expect(result.data).toContain("MORE2"); expect(result.data).toContain("# 📋 JIRA Projects"); }); }); describe("expand options", () => { test("should expand project details", async () => { const params: GetProjectsParams = { expand: ["description", "lead", "issueTypes"], maxResults: 50, startAt: 0, }; const mockProjects = [ { ...mockFactory.createMockProject({ key: "EXP1", name: "Expanded Project", }), description: "This is a detailed project description", lead: { accountId: "lead-123", displayName: "Project Lead", emailAddress: "lead@company.com", }, issueTypes: [ { id: "1", name: "Bug", iconUrl: "bug.png" }, { id: "2", name: "Task", iconUrl: "task.png" }, ], }, ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("EXP1"); expect(result.data).toContain("Expanded Project"); expect(result.data).toContain("detailed project description"); expect(result.data).toContain("Project Lead"); }); test("should handle multiple expand options", async () => { const params: GetProjectsParams = { expand: ["description", "lead", "url"], maxResults: 50, startAt: 0, }; const mockProjects = [ { ...mockFactory.createMockProject({ key: "MULTI", name: "Multi Expand Project", }), description: "Multi-expand description", lead: { accountId: "multi-lead", displayName: "Multi Lead", emailAddress: "multi@company.com", }, url: "https://company.atlassian.net/projects/MULTI", }, ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("MULTI"); expect(result.data).toContain("Multi-expand description"); expect(result.data).toContain("Multi Lead"); }); }); describe("error handling", () => { test("should handle permission denied error", async () => { // Setup the use case to throw a JiraPermissionError ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockImplementation(() => { throw new JiraPermissionError("Forbidden - insufficient permissions"); }); const result = await handler.handle({}); expect(result.success).toBe(false); expect(result.error).toContain("Permission Denied"); }); test("should handle authentication error", async () => { // Setup the use case to throw a JiraApiError ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockImplementation(() => { throw JiraApiError.withStatusCode("Authentication failed", 401); }); const result = await handler.handle({}); expect(result.success).toBe(false); expect(result.error).toContain("JIRA API Error"); expect(result.error).toContain("Authentication failed"); }); test("should handle invalid search query", async () => { const params: GetProjectsParams = { searchQuery: "invalid[query", maxResults: 50, startAt: 0, }; // Setup the use case to throw a JiraApiError ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockImplementation(() => { throw JiraApiError.withStatusCode("Invalid search query syntax", 400); }); const result = await handler.handle(params); expect(result.success).toBe(false); expect(result.error).toContain("JIRA API Error"); expect(result.error).toContain("search query syntax"); }); test("should reject invalid project type key", async () => { const params: GetProjectsParams = { typeKey: "software", maxResults: 10, startAt: 0, }; // Setup the use case to throw an error for invalid parameters ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockImplementation(async () => { throw new Error("Invalid typeKey"); }); const result = await handler.handle(params); expect(result.success).toBe(false); expect(result.error).toContain("Project Retrieval Failed"); expect(result.error).toContain("Invalid typeKey"); }); test("should handle network errors gracefully", async () => { // Setup the use case to throw a network error ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockImplementation(() => { throw new Error("Network error occurred"); }); const result = await handler.handle({}); expect(result.success).toBe(false); expect(result.error).toContain("Project Retrieval Failed"); expect(result.error).toContain("Network error"); }); test("should handle missing dependencies error", async () => { // Create handler with missing dependencies const handlerWithoutDependencies = new GetProjectsHandler( undefined as unknown as GetProjectsUseCase, mockValidator, ); const result = await handlerWithoutDependencies.handle({}); expect(result.success).toBe(false); expect(result.error).toContain("Project Retrieval Failed"); }); }); describe("parameter validation", () => { test("should validate maxResults range", async () => { const params = { maxResults: -1, startAt: 0, } as GetProjectsParams; // Setup the validator to throw an error for invalid parameters mockValidator.validateGetProjectsParams = mock(() => { throw JiraApiError.withStatusCode( "Invalid project retrieval parameters", 400, ); }); const result = await handler.handle(params); expect(result.success).toBe(false); expect(result.error).toContain("JIRA API Error"); expect(result.error).toContain("Invalid project retrieval parameters"); }); test("should validate startAt value", async () => { const params = { maxResults: 50, startAt: -5, } as GetProjectsParams; // Setup the validator to throw an error for invalid parameters mockValidator.validateGetProjectsParams = mock(() => { throw JiraApiError.withStatusCode( "Invalid project retrieval parameters", 400, ); }); const result = await handler.handle(params); expect(result.success).toBe(false); expect(result.error).toContain("JIRA API Error"); expect(result.error).toContain("Invalid project retrieval parameters"); }); test("should validate orderBy values", async () => { const params = { maxResults: 50, startAt: 0, orderBy: "invalid-order", } as unknown as GetProjectsParams; // Setup the validator to throw an error for invalid parameters mockValidator.validateGetProjectsParams = mock(() => { throw JiraApiError.withStatusCode( "Invalid project retrieval parameters", 400, ); }); const result = await handler.handle(params); expect(result.success).toBe(false); expect(result.error).toContain("JIRA API Error"); expect(result.error).toContain("Invalid project retrieval parameters"); }); test("should reject invalid expand options", async () => { const params = { maxResults: 10, startAt: 0, expand: ["description", "lead"] as const, }; // Setup the use case to throw an error for invalid parameters ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockImplementation(async () => { throw new Error("Invalid expand options"); }); const result = await handler.handle(params); expect(result.success).toBe(false); expect(result.error).toContain("Project Retrieval Failed"); expect(result.error).toContain("Invalid expand options"); }); test("should accept valid parameters", async () => { const params: GetProjectsParams = { searchQuery: "test", typeKey: "software", categoryId: 10001, recent: 5, orderBy: "name", maxResults: 50, startAt: 0, expand: ["description", "lead"], }; const mockProjects = [ mockFactory.createMockProject({ key: "VALID", name: "Valid Project" }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = await handler.handle(params); expect(result.success).toBe(true); expect(result.data).toContain("VALID"); }); }); describe("edge cases", () => { test("should handle very large result sets", async () => { const params: GetProjectsParams = { maxResults: 50, // Changed from 100 to 50 to match the valid range startAt: 0, }; // Create 50 mock projects (instead of 100) const mockProjects = Array.from({ length: 50 }, (_, i) => mockFactory.createMockProject({ key: `LARGE${i + 1}`, name: `Large Project ${i + 1}`, }), ); // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("LARGE1"); expect(result.data).toContain("LARGE50"); // Changed from LARGE100 to LARGE50 expect(result.data).toContain("50 projects"); // Changed from 100 to 50 }); test("should handle projects with minimal data", async () => { const mockProjects = [ { id: "minimal-1", key: "MIN", name: "Minimal Project", projectTypeKey: "software", style: "classic", // Add the required style property simplified: false, isPrivate: false, // Missing optional fields }, ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects as Project[]); const result = (await handler.handle({})) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("MIN"); expect(result.data).toContain("Minimal Project"); }); test("should handle projects with special characters", async () => { const mockProjects = [ mockFactory.createMockProject({ key: "SPEC", name: "Special Chars: @#$%^&*()_+-=[]{}|;':\",./<>?", }), ]; // Setup mock use case to return projects ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue(mockProjects); const result = (await handler.handle({})) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("SPEC"); expect(result.data).toContain("Special Chars"); }); test("should handle empty search results with filters", async () => { const params: GetProjectsParams = { searchQuery: "nonexistent", typeKey: "software", maxResults: 50, startAt: 0, }; // Setup mock use case to return empty array ( mockUseCase.execute as Mock< (params: GetProjectsParams) => Promise<Project[]> > ).mockResolvedValue([]); const result = (await handler.handle(params)) as McpResponse<string>; expect(result.success).toBe(true); expect(result.data).toContain("No Projects Found"); expect(result.data).toContain("No projects are accessible"); }); }); });

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/Dsazz/mcp-jira'

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