JIRA MCP Server

by cosmix
import { expect, test, describe, beforeEach, afterEach } from "bun:test"; import { JiraApiService } from "../jira-api.js"; const mockFormattedDescription = { fields: { description: { content: [ { type: "paragraph", content: [ { type: "text", text: "As a ", }, { type: "text", text: "user", marks: [{ type: "em" }], }, { type: "text", text: " I want to see formatted text", }, ], }, ], }, }, }; describe("JiraApiService", () => { describe("cleanIssue", () => { test("should properly handle formatted text in description", () => { const service = new JiraApiService( "http://test", "test@test.com", "token", ); const result = (service as any).cleanIssue(mockFormattedDescription); expect(result.description).toBe("As a user I want to see formatted text"); }); }); const baseUrl = "https://your-domain.atlassian.net"; const apiToken = "test-token"; const email = "user@domain.net"; let service: JiraApiService; let originalFetch: typeof fetch; beforeEach(() => { service = new JiraApiService(baseUrl, email, apiToken); originalFetch = global.fetch; }); afterEach(() => { global.fetch = originalFetch; }); describe("constructor", () => { test("should set up fetch with correct base URL and auth header", async () => { // Mock fetch to verify headers const mockFetch = async ( input: RequestInfo | URL, init?: RequestInit, ) => { const url = input.toString(); expect(url.startsWith(baseUrl)).toBe(true); const headers = init?.headers as Headers; expect(headers.get("Authorization")).toBe( `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, ); expect(headers.get("Content-Type")).toBe("application/json"); return new Response(JSON.stringify({ issues: [] })); }; mockFetch.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch; await service.searchIssues("project = TEST"); }); }); describe("searchIssues", () => { test("should make GET request to correct endpoint and clean response", async () => { const mockResponse = { issues: [ { id: "1", key: "TEST-1", fields: { summary: "Test Issue", status: { name: "Open" }, created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", parent: { id: "parent-1", key: "TEST-PARENT", fields: { summary: "Parent Issue", }, }, subtasks: [ { id: "child-1", key: "TEST-CHILD", fields: { summary: "Child Issue", }, }, ], customfield_10014: "EPIC-1", description: { content: [ { type: "paragraph", content: [ { type: "text", text: "Test Description with mention of TEST-2", }, { type: "inlineCard", attrs: { url: "/browse/TEST-3", }, }, ], }, ], }, issuelinks: [ { type: { inward: "is blocked by", }, inwardIssue: { key: "TEST-4", fields: { summary: "Blocking Issue", }, }, }, ], }, }, ], total: 1, }; const expectedResponse = { total: 1, issues: [ { id: "1", key: "TEST-1", summary: "Test Issue", description: "Test Description with mention of TEST-2", status: "Open", created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", parent: { id: "parent-1", key: "TEST-PARENT", summary: "Parent Issue", }, children: [ { id: "child-1", key: "TEST-CHILD", summary: "Child Issue", }, ], epicLink: { id: "EPIC-1", key: "EPIC-1", summary: undefined, }, relatedIssues: [ { key: "TEST-2", type: "mention" as const, source: "description" as const, }, { key: "TEST-3", type: "mention" as const, source: "description" as const, }, { key: "TEST-4", summary: "Blocking Issue", type: "link" as const, relationship: "is blocked by", source: "description" as const, }, ], }, ], }; const mockFetch1 = async () => new Response(JSON.stringify(mockResponse)); mockFetch1.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch1; const result = await service.searchIssues("project = TEST"); expect(result).toEqual(expectedResponse); }); test("should handle error responses", async () => { const mockFetch2 = async () => new Response( JSON.stringify({ message: "You do not have permission" }), { status: 403 }, ); mockFetch2.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch2; await expect(service.searchIssues("project = TEST")).rejects.toThrow( "JIRA API Error: You do not have permission", ); }); }); describe("getEpicChildren", () => { const epicKey = "TEST-1"; const mockResponse = { issues: [ { id: "2", key: "TEST-2", fields: { summary: "Child Issue", description: { content: [ { type: "paragraph", content: [ { type: "text", text: "Child Description", }, ], }, ], }, status: { name: "Open" }, created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", }, }, ], total: 1, }; const mockComments = { comments: [ { id: "1", body: { content: [ { type: "paragraph", content: [ { type: "text", text: "Test Comment mentioning TEST-5", }, { type: "inlineCard", attrs: { url: "/browse/TEST-6", }, }, ], }, ], }, author: { displayName: "Test User" }, created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", }, ], }; test("should fetch epic children with comments", async () => { let fetchCount = 0; const mockFetch3 = async (input: RequestInfo | URL) => { const url = input.toString(); if (url.includes("/search")) { return new Response(JSON.stringify(mockResponse)); } if (url.includes("/comment")) { return new Response(JSON.stringify(mockComments)); } throw new Error(`Unexpected URL: ${url}`); }; mockFetch3.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch3; const expectedResponse = [ { id: "2", key: "TEST-2", summary: "Child Issue", description: "Child Description", status: "Open", created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", comments: [ { id: "1", body: "Test Comment mentioning TEST-5", author: "Test User", created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", mentions: [ { key: "TEST-5", type: "mention" as const, source: "comment" as const, commentId: "1", }, { key: "TEST-6", type: "mention" as const, source: "comment" as const, commentId: "1", }, ], }, ], relatedIssues: [ { key: "TEST-5", type: "mention" as const, source: "comment" as const, commentId: "1", }, { key: "TEST-6", type: "mention" as const, source: "comment" as const, commentId: "1", }, ], }, ]; const result = await service.getEpicChildren(epicKey); expect(result).toEqual(expectedResponse); }); test("should handle error responses", async () => { const mockFetch4 = async () => new Response( JSON.stringify({ message: "You do not have permission" }), { status: 403 }, ); mockFetch4.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch4; await expect(service.getEpicChildren(epicKey)).rejects.toThrow( "JIRA API Error: You do not have permission", ); }); }); describe("getIssueWithComments", () => { const issueId = "TEST-1"; const mockIssue = { id: "1", key: "TEST-1", fields: { summary: "Test Issue", description: { content: [ { type: "paragraph", content: [ { type: "text", text: "Test Description with mention of TEST-7", }, ], }, ], }, status: { name: "Open" }, created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", parent: { id: "parent-1", key: "TEST-PARENT", fields: { summary: "Parent Issue", }, }, subtasks: [ { id: "child-1", key: "TEST-CHILD", fields: { summary: "Child Issue", }, }, ], customfield_10014: "EPIC-1", issuelinks: [ { type: { outward: "blocks", }, outwardIssue: { key: "TEST-8", fields: { summary: "Blocked Issue", }, }, }, ], }, }; const mockEpic = { fields: { summary: "Epic Issue", }, }; const mockComments = { comments: [ { id: "1", body: { content: [ { type: "paragraph", content: [ { type: "text", text: "Test Comment mentioning TEST-9", }, ], }, ], }, author: { displayName: "Test User" }, created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", }, ], }; test("should make parallel requests for issue, comments, and epic details", async () => { const mockFetch5 = async (input: RequestInfo | URL) => { const url = input.toString(); if (url.includes(`/issue/${issueId}?`)) { return new Response(JSON.stringify(mockIssue)); } if (url.includes("/comment")) { return new Response(JSON.stringify(mockComments)); } if (url.includes("/issue/EPIC-1")) { return new Response(JSON.stringify(mockEpic)); } throw new Error(`Unexpected URL: ${url}`); }; mockFetch5.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch5; const result = await service.getIssueWithComments(issueId); expect(result).toEqual({ id: "1", key: "TEST-1", summary: "Test Issue", description: "Test Description with mention of TEST-7", status: "Open", created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", parent: { id: "parent-1", key: "TEST-PARENT", summary: "Parent Issue", }, children: [ { id: "child-1", key: "TEST-CHILD", summary: "Child Issue", }, ], epicLink: { id: "EPIC-1", key: "EPIC-1", summary: "Epic Issue", }, comments: [ { id: "1", body: "Test Comment mentioning TEST-9", author: "Test User", created: "2024-01-01T00:00:00.000Z", updated: "2024-01-01T00:00:00.000Z", mentions: [ { key: "TEST-9", type: "mention" as const, source: "comment" as const, commentId: "1", }, ], }, ], relatedIssues: [ { key: "TEST-7", type: "mention" as const, source: "description" as const, }, { key: "TEST-8", summary: "Blocked Issue", type: "link" as const, relationship: "blocks", source: "description" as const, }, { key: "TEST-9", type: "mention" as const, source: "comment" as const, commentId: "1", }, ], }); }); test("should handle epic fetch failure gracefully", async () => { const mockFetch6 = async (input: RequestInfo | URL) => { const url = input.toString(); if (url.includes(`/issue/${issueId}?`)) { return new Response(JSON.stringify(mockIssue)); } if (url.includes("/comment")) { return new Response(JSON.stringify(mockComments)); } if (url.includes("/issue/EPIC-1")) { return new Response("Not Found", { status: 404 }); } throw new Error(`Unexpected URL: ${url}`); }; mockFetch6.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch6; const result = await service.getIssueWithComments(issueId); expect(result.epicLink?.summary).toBeUndefined(); }); test("should handle 404 errors correctly", async () => { const mockFetch7 = async () => new Response("Not Found", { status: 404 }); mockFetch7.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch7; await expect(service.getIssueWithComments(issueId)).rejects.toThrow( `Issue not found: ${issueId}`, ); }); test("should handle permission errors", async () => { const mockFetch8 = async () => new Response( JSON.stringify({ message: "You do not have permission to view this issue", }), { status: 403 }, ); mockFetch8.preconnect = async () => {}; // Add dummy preconnect global.fetch = mockFetch8; await expect(service.getIssueWithComments(issueId)).rejects.toThrow( "JIRA API Error: You do not have permission to view this issue", ); }); }); describe("addCommentToIssue", () => { const issueIdOrKey = "TEST-10"; const commentBody = "This is a new comment."; const expectedAdf = { version: 1, type: "doc", content: [ { type: "paragraph", content: [ { type: "text", text: commentBody, }, ], }, ], }; const mockApiResponse: any = { // Using 'any' for brevity in mock id: "12345", self: `${baseUrl}/rest/api/3/issue/${issueIdOrKey}/comment/12345`, author: { displayName: "Test User" }, body: expectedAdf, // JIRA returns ADF created: "2024-01-02T00:00:00.000Z", updated: "2024-01-02T00:00:00.000Z", }; const expectedCleanResponse = { id: "12345", author: "Test User", created: "2024-01-02T00:00:00.000Z", updated: "2024-01-02T00:00:00.000Z", body: commentBody, // Cleaned response has plain text }; test("should send POST request with correct ADF body and return cleaned response on success", async () => { const mockFetch = async ( input: RequestInfo | URL, init?: RequestInit, ) => { const url = input.toString(); expect(url).toBe(`${baseUrl}/rest/api/3/issue/${issueIdOrKey}/comment`); expect(init?.method).toBe("POST"); expect(init?.headers).toEqual(service["headers"]); // Check if headers match the instance headers const sentBody = JSON.parse(init?.body as string); expect(sentBody.body).toEqual(expectedAdf); // Verify the ADF structure return new Response(JSON.stringify(mockApiResponse), { status: 201 }); }; mockFetch.preconnect = async () => {}; global.fetch = mockFetch; const result = await service.addCommentToIssue(issueIdOrKey, commentBody); expect(result).toEqual(expectedCleanResponse); }); test("should handle 404 Not Found error", async () => { const mockFetch = async () => new Response( JSON.stringify({ errorMessages: [ "Issue does not exist or you do not have permission to see it.", ], }), { status: 404 }, ); mockFetch.preconnect = async () => {}; global.fetch = mockFetch; await expect( service.addCommentToIssue(issueIdOrKey, commentBody), ).rejects.toThrow( "JIRA API Error: Issue does not exist or you do not have permission to see it.", ); }); test("should handle 403 Forbidden error", async () => { const mockFetch = async () => new Response( JSON.stringify({ errorMessages: [ "User does not have permission to comment on this issue.", ], }), { status: 403 }, ); mockFetch.preconnect = async () => {}; global.fetch = mockFetch; await expect( service.addCommentToIssue(issueIdOrKey, commentBody), ).rejects.toThrow( "JIRA API Error: User does not have permission to comment on this issue.", ); }); test("should handle 400 Bad Request error", async () => { const mockFetch = async () => new Response( JSON.stringify({ errorMessages: ["Invalid request body"] }), { status: 400 }, ); mockFetch.preconnect = async () => {}; global.fetch = mockFetch; await expect( service.addCommentToIssue(issueIdOrKey, commentBody), ).rejects.toThrow("JIRA API Error: Invalid request body"); }); }); });
ID: q659niazed