Skip to main content
Glama
linear-project.test.ts11.4 kB
import { linearClient } from '../linear.js'; import { linearProjectResourceHandler, linearProjectIssuesResourceHandler, LinearProjectDetailResponse, LinearProjectIssuesResponse, LinearProjectMilestonesResponse, linearProjectMilestonesResourceHandler, } from './linear-project.js'; // Mock the Linear client jest.mock('../linear.js', () => ({ linearClient: { project: jest.fn(), }, })); describe('Linear Project Resource Handlers', () => { // Common mock data const mockStatus = { id: Promise.resolve('status1'), name: Promise.resolve('Active'), color: Promise.resolve('#00ff00'), type: Promise.resolve('started'), }; const mockLead = { id: Promise.resolve('user1'), name: Promise.resolve('John Doe'), displayName: Promise.resolve('John'), }; const mockTeam1 = { id: 'team1', name: 'Engineering', key: 'ENG', }; const mockTeam2 = { id: 'team2', name: 'Design', key: 'DSG', }; const mockMember1 = { id: 'member1', name: 'Jane Smith', displayName: 'Jane', }; const mockMember2 = { id: 'member2', name: 'Bob Johnson', displayName: 'Bob', }; // Mock project const mockProject = { id: 'project123', name: 'Test Project', description: 'A test project', color: '#ff0000', icon: '🚀', progress: 0.5, status: Promise.resolve(mockStatus), lead: Promise.resolve(mockLead), teams: jest.fn().mockResolvedValue({ nodes: [mockTeam1, mockTeam2], }), members: jest.fn().mockResolvedValue({ nodes: [mockMember1, mockMember2], }), startDate: '2023-01-01', targetDate: '2023-12-31', createdAt: new Date('2023-01-01'), updatedAt: new Date('2023-01-02'), issues: jest.fn(), }; // Mock issues const mockIssue1 = { id: 'issue1', title: 'Fix bug', identifier: 'PROJ-1', description: 'Need to fix a critical bug', state: Promise.resolve({ name: 'Todo' }), priority: 1, assignee: Promise.resolve({ id: 'user1', name: 'John Doe', displayName: 'John', }), url: 'https://linear.app/team/issue/PROJ-1', createdAt: new Date('2023-01-05'), updatedAt: new Date('2023-01-06'), }; const mockIssue2 = { id: 'issue2', title: 'Implement feature', identifier: 'PROJ-2', description: 'Implement new feature', state: Promise.resolve({ name: 'In Progress' }), priority: 2, assignee: Promise.resolve(null), url: 'https://linear.app/team/issue/PROJ-2', createdAt: new Date('2023-01-07'), updatedAt: new Date('2023-01-08'), }; beforeEach(() => { jest.clearAllMocks(); (linearClient.project as jest.Mock).mockResolvedValue(mockProject); mockProject.issues.mockResolvedValue({ nodes: [mockIssue1, mockIssue2], }); }); describe('Linear Project Resource Handler', () => { it('should return project details when given a valid project ID', async () => { const result = await linearProjectResourceHandler({ projectId: 'project123' }); expect(linearClient.project).toHaveBeenCalledWith('project123'); expect(result.isError).toBe(false); const data = result.data as LinearProjectDetailResponse; expect(data).toMatchObject({ id: 'project123', name: 'Test Project', description: 'A test project', color: '#ff0000', icon: '🚀', progress: 0.5, startDate: '2023-01-01', targetDate: '2023-12-31', }); // Check nested objects expect(data.status).toBeDefined(); expect(data.status?.name).toBe('Active'); expect(data.status?.color).toBe('#00ff00'); expect(data.lead).toBeDefined(); expect(data.lead?.name).toBe('John Doe'); expect(data.lead?.displayName).toBe('John'); // Check teams and members expect(data.teams).toHaveLength(2); expect(data.teams[0].key).toBe('ENG'); expect(data.teams[1].name).toBe('Design'); expect(data.members).toHaveLength(2); expect(data.members[0].displayName).toBe('Jane'); expect(data.members[1].name).toBe('Bob Johnson'); // Check dates expect(data.createdAt).toEqual(new Date('2023-01-01')); expect(data.updatedAt).toEqual(new Date('2023-01-02')); }); it('should return an error when project ID is missing', async () => { const result = await linearProjectResourceHandler({}); expect(linearClient.project).not.toHaveBeenCalled(); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Invalid project ID'); expect(result.data).toBeNull(); }); it('should return an error when project is not found', async () => { (linearClient.project as jest.Mock).mockResolvedValue(null); const result = await linearProjectResourceHandler({ projectId: 'nonexistent' }); expect(linearClient.project).toHaveBeenCalledWith('nonexistent'); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Project with ID nonexistent not found'); expect(result.data).toBeNull(); }); it('should handle missing related entities', async () => { const mockPartialProject = { ...mockProject, status: Promise.resolve(null), lead: Promise.resolve(null), teams: jest.fn().mockResolvedValue({ nodes: [] }), members: jest.fn().mockResolvedValue({ nodes: [] }), }; (linearClient.project as jest.Mock).mockResolvedValue(mockPartialProject); const result = await linearProjectResourceHandler({ projectId: 'project123' }); expect(result.isError).toBe(false); const data = result.data as LinearProjectDetailResponse; expect(data.status).toBeUndefined(); expect(data.lead).toBeUndefined(); expect(data.teams).toHaveLength(0); expect(data.members).toHaveLength(0); }); it('should handle API errors gracefully', async () => { (linearClient.project as jest.Mock).mockRejectedValue(new Error('API error')); const result = await linearProjectResourceHandler({ projectId: 'project123' }); expect(linearClient.project).toHaveBeenCalledWith('project123'); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Error: API error'); expect(result.data).toBeNull(); }); }); describe('Linear Project Issues Resource Handler', () => { it('should return project issues when given a valid project ID', async () => { const result = await linearProjectIssuesResourceHandler({ projectId: 'project123' }); expect(linearClient.project).toHaveBeenCalledWith('project123'); expect(mockProject.issues).toHaveBeenCalledWith({ first: 50 }); expect(result.isError).toBe(false); const data = result.data as LinearProjectIssuesResponse; // Check project data expect(data.project).toMatchObject({ id: 'project123', name: 'Test Project', description: 'A test project', }); // Check issues expect(data.issues).toHaveLength(2); // Check first issue expect(data.issues[0]).toMatchObject({ id: 'issue1', title: 'Fix bug', identifier: 'PROJ-1', description: 'Need to fix a critical bug', status: 'Todo', priority: 1, }); expect(data.issues[0].assignee).toBeDefined(); expect(data.issues[0].assignee?.name).toBe('John Doe'); // Check second issue expect(data.issues[1]).toMatchObject({ id: 'issue2', title: 'Implement feature', identifier: 'PROJ-2', status: 'In Progress', priority: 2, }); expect(data.issues[1].assignee).toBeUndefined(); }); it('should return an error when project ID is missing', async () => { const result = await linearProjectIssuesResourceHandler({}); expect(linearClient.project).not.toHaveBeenCalled(); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Invalid project ID'); expect(result.data).toBeNull(); }); it('should return an error when project is not found', async () => { (linearClient.project as jest.Mock).mockResolvedValue(null); const result = await linearProjectIssuesResourceHandler({ projectId: 'nonexistent' }); expect(linearClient.project).toHaveBeenCalledWith('nonexistent'); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Project with ID nonexistent not found'); expect(result.data).toBeNull(); }); it('should handle empty issues list', async () => { mockProject.issues.mockResolvedValue({ nodes: [] }); const result = await linearProjectIssuesResourceHandler({ projectId: 'project123' }); expect(result.isError).toBe(false); const data = result.data as LinearProjectIssuesResponse; expect(data.issues).toHaveLength(0); }); it('should handle API errors gracefully', async () => { mockProject.issues.mockRejectedValue(new Error('API error')); const result = await linearProjectIssuesResourceHandler({ projectId: 'project123' }); expect(linearClient.project).toHaveBeenCalledWith('project123'); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Error: API error'); expect(result.data).toBeNull(); }); }); describe('linear-project-milestones resource', () => { it('should return project milestones for a valid project ID', async () => { const result = await linearProjectMilestonesResourceHandler({ projectId: 'project123' }); expect(result.isError).toBe(false); expect(result.data).toBeDefined(); const data = result.data as LinearProjectMilestonesResponse; expect(data.project.id).toBe('project123'); expect(data.project.name).toBe('Test Project'); expect(data.milestones).toHaveLength(3); // Check first milestone expect(data.milestones[0].name).toBe('Alpha Release'); expect(data.milestones[0].status).toBe('completed'); expect(data.milestones[0].issues.total).toBe(5); expect(data.milestones[0].issues.completed).toBe(5); // Check second milestone expect(data.milestones[1].name).toBe('Beta Release'); expect(data.milestones[1].status).toBe('inProgress'); expect(data.milestones[1].issues.total).toBe(8); expect(data.milestones[1].issues.completed).toBe(5); // Check statistics expect(data.stats.total).toBe(3); expect(data.stats.byStatus.completed).toBe(1); expect(data.stats.byStatus.inProgress).toBe(1); expect(data.stats.byStatus.planned).toBe(1); expect(data.stats.completed).toBe(1); }); it('should handle invalid project ID', async () => { const result = await linearProjectMilestonesResourceHandler({ projectId: '' }); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Invalid project ID'); expect(result.data).toBeNull(); }); it('should handle non-existent project ID', async () => { (linearClient.project as jest.Mock).mockResolvedValue(null); const result = await linearProjectMilestonesResourceHandler({ projectId: 'nonexistent' }); expect(result.isError).toBe(true); expect(result.errorMessage).toBe('Project with ID nonexistent not found'); expect(result.data).toBeNull(); }); }); });

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/magarcia/mcp-server-linearapp'

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