Linear MCP Server
by cpropster
- src
- __tests__
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { LinearGraphQLClient } from '../graphql/client';
import { LinearClient } from '@linear/sdk';
import {
CreateIssueInput,
CreateIssueResponse,
CreateIssuesResponse,
UpdateIssueInput,
UpdateIssuesResponse,
SearchIssuesInput,
SearchIssuesResponse,
DeleteIssueResponse
} from '../features/issues/types/issue.types';
import {
ProjectInput,
ProjectResponse,
SearchProjectsResponse
} from '../features/projects/types/project.types';
import {
TeamResponse,
LabelInput,
LabelResponse
} from '../features/teams/types/team.types';
import {
UserResponse
} from '../features/users/types/user.types';
jest.mock('@linear/sdk');
// Define type for GraphQL response
type GraphQLResponse<T> = {
data: T;
};
describe('LinearGraphQLClient', () => {
let graphqlClient: LinearGraphQLClient;
let linearClient: LinearClient;
let mockRawRequest: jest.MockedFunction<(query: string, variables?: Record<string, unknown>) => Promise<GraphQLResponse<unknown>>>;
beforeEach(() => {
mockRawRequest = jest.fn();
// Mock the Linear client's GraphQL client
linearClient = {
client: {
rawRequest: mockRawRequest
}
} as unknown as LinearClient;
// Clear mocks
mockRawRequest.mockReset();
graphqlClient = new LinearGraphQLClient(linearClient);
});
describe('searchIssues', () => {
it('should successfully search issues', async () => {
const mockResponse = {
data: {
issues: {
pageInfo: {
hasNextPage: false,
endCursor: null
},
nodes: [
{
id: 'issue-1',
identifier: 'TEST-1',
title: 'Test Issue 1',
url: 'https://linear.app/test/issue/TEST-1'
}
]
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const searchInput: SearchIssuesInput = {
filter: {
project: {
id: {
eq: 'project-1'
}
}
},
first: 1
};
const result: SearchIssuesResponse = await graphqlClient.searchIssues(
searchInput.filter,
searchInput.first
);
expect(result).toEqual(mockResponse.data);
expect(mockRawRequest).toHaveBeenCalled();
});
it('should handle search errors', async () => {
mockRawRequest.mockRejectedValueOnce(new Error('Search failed'));
const searchInput: SearchIssuesInput = {
filter: {
project: {
id: {
eq: 'project-1'
}
}
}
};
await expect(
graphqlClient.searchIssues(searchInput.filter)
).rejects.toThrow('GraphQL operation failed: Search failed');
});
});
describe('createIssue', () => {
it('should successfully create an issue', async () => {
const mockResponse = {
data: {
issueCreate: {
success: true,
issue: {
id: 'issue-1',
identifier: 'TEST-1',
title: 'New Issue',
url: 'https://linear.app/test/issue/TEST-1'
}
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const input: CreateIssueInput = {
title: 'New Issue',
description: 'Description',
teamId: 'team-1'
};
const result: CreateIssueResponse = await graphqlClient.createIssue(input);
// Verify single mutation call with array input
expect(mockRawRequest).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
input: [input]
})
);
expect(result).toEqual(mockResponse.data);
expect(mockRawRequest).toHaveBeenCalled();
});
it('should handle creation errors', async () => {
mockRawRequest.mockRejectedValueOnce(new Error('Creation failed'));
const input: CreateIssueInput = {
title: 'New Issue',
description: 'Description',
teamId: 'team-1'
};
await expect(
graphqlClient.createIssue(input)
).rejects.toThrow('GraphQL operation failed: Creation failed');
});
});
describe('Project Operations', () => {
describe('createProject', () => {
it('should successfully create a project', async () => {
const mockResponse = {
data: {
projectCreate: {
success: true,
project: {
id: 'project-1',
name: 'New Project',
url: 'https://linear.app/test/project/1'
},
lastSyncId: 123
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const projectInput: ProjectInput = {
name: 'New Project',
teamIds: ['team-1']
};
const result = await graphqlClient.createProject(projectInput);
expect(result).toEqual(mockResponse.data);
expect(mockRawRequest).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ input: projectInput })
);
});
});
describe('createProjectWithIssues', () => {
it('should successfully create project with issues', async () => {
const projectMockResponse = {
data: {
projectCreate: {
success: true,
project: {
id: 'project-1',
name: 'New Project',
url: 'https://linear.app/test/project/1'
},
lastSyncId: 123
}
}
};
const issueMockResponse = {
data: {
issueBatchCreate: {
success: true,
issues: [
{
id: 'issue-1',
identifier: 'TEST-1',
title: 'Project Issue 1',
url: 'https://linear.app/test/issue/TEST-1'
}
],
lastSyncId: 124
}
}
};
mockRawRequest
.mockResolvedValueOnce(projectMockResponse)
.mockResolvedValueOnce(issueMockResponse);
const projectInput: ProjectInput = {
name: 'New Project',
teamIds: ['team-1']
};
const issueInput: CreateIssueInput = {
title: 'Project Issue 1',
description: 'Description 1',
teamId: 'team-1'
};
const result = await graphqlClient.createProjectWithIssues(
projectInput,
[issueInput]
);
expect(result).toEqual({
projectCreate: projectMockResponse.data.projectCreate,
issueBatchCreate: issueMockResponse.data.issueBatchCreate
});
// Verify project creation call
expect(mockRawRequest).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ input: projectInput })
);
// Verify issue creation call
expect(mockRawRequest).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
input: {
issues: [{ ...issueInput, projectId: 'project-1' }]
}
})
);
});
it('should handle project creation errors', async () => {
const errorResponse = {
data: {
projectCreate: {
success: false,
project: null,
lastSyncId: 123
}
}
};
mockRawRequest.mockResolvedValueOnce(errorResponse);
const projectInput: ProjectInput = {
name: 'New Project',
teamIds: ['team-1']
};
const issueInput: CreateIssueInput = {
title: 'Project Issue 1',
description: 'Description 1',
teamId: 'team-1'
};
await expect(
graphqlClient.createProjectWithIssues(projectInput, [issueInput])
).rejects.toThrow('Failed to create project');
});
it('should handle issue creation errors', async () => {
const projectResponse = {
data: {
projectCreate: {
success: true,
project: {
id: 'project-1',
name: 'New Project',
url: 'https://linear.app/test/project/1'
},
lastSyncId: 123
}
}
};
const errorResponse = {
data: {
issueBatchCreate: {
success: false,
issues: [],
lastSyncId: 124
}
}
};
mockRawRequest
.mockResolvedValueOnce(projectResponse)
.mockResolvedValueOnce(errorResponse);
const projectInput: ProjectInput = {
name: 'New Project',
teamIds: ['team-1']
};
const issueInput: CreateIssueInput = {
title: 'Project Issue 1',
description: 'Description 1',
teamId: 'team-1'
};
await expect(
graphqlClient.createProjectWithIssues(projectInput, [issueInput])
).rejects.toThrow('Failed to create issues');
});
});
});
describe('Bulk Operations', () => {
it('should create multiple issues with a single mutation', async () => {
const mockResponse = {
data: {
issueCreate: {
success: true,
issues: [
{
id: 'issue-1',
identifier: 'TEST-1',
title: 'Issue 1',
url: 'https://linear.app/test/issue/TEST-1'
},
{
id: 'issue-2',
identifier: 'TEST-2',
title: 'Issue 2',
url: 'https://linear.app/test/issue/TEST-2'
}
]
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const issues: CreateIssueInput[] = [
{
title: 'Issue 1',
description: 'Description 1',
teamId: 'team-1'
},
{
title: 'Issue 2',
description: 'Description 2',
teamId: 'team-1'
}
];
const result: CreateIssuesResponse = await graphqlClient.createIssues(issues);
expect(result).toEqual(mockResponse.data);
// Verify single mutation call
expect(mockRawRequest).toHaveBeenCalledTimes(1);
expect(mockRawRequest).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
input: issues
})
);
});
it('should update multiple issues with a single mutation', async () => {
const mockResponse = {
data: {
issueUpdate: {
success: true,
issues: [
{
id: 'issue-1',
identifier: 'TEST-1',
title: 'Updated Issue 1',
url: 'https://linear.app/test/issue/TEST-1'
},
{
id: 'issue-2',
identifier: 'TEST-2',
title: 'Updated Issue 2',
url: 'https://linear.app/test/issue/TEST-2'
}
]
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const ids = ['issue-1', 'issue-2'];
const updateInput: UpdateIssueInput = { stateId: 'state-2' };
const result: UpdateIssuesResponse = await graphqlClient.updateIssues(ids, updateInput);
expect(result).toEqual(mockResponse.data);
// Verify single mutation call
expect(mockRawRequest).toHaveBeenCalledTimes(1);
expect(mockRawRequest).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
ids,
input: updateInput
})
);
});
it('should handle update errors', async () => {
mockRawRequest.mockRejectedValueOnce(new Error('Update failed'));
const updateInput: UpdateIssueInput = { stateId: 'state-2' };
await expect(
graphqlClient.updateIssues(['issue-1'], updateInput)
).rejects.toThrow('GraphQL operation failed: Update failed');
});
it('should delete multiple issues with a single mutation', async () => {
const mockResponse = {
data: {
issueDelete: {
success: true
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const ids = ['issue-1', 'issue-2'];
const result: DeleteIssueResponse = await graphqlClient.deleteIssues(ids);
expect(result).toEqual(mockResponse.data);
// Verify single mutation call
expect(mockRawRequest).toHaveBeenCalledTimes(1);
expect(mockRawRequest).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
ids
})
);
});
});
describe('getTeams', () => {
it('should successfully fetch teams', async () => {
const mockResponse = {
data: {
teams: {
nodes: [
{
id: 'team-1',
name: 'Team 1',
key: 'TEAM1',
states: [
{
id: 'state-1',
name: 'Todo',
type: 'unstarted'
}
]
}
]
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const result: TeamResponse = await graphqlClient.getTeams();
expect(result).toEqual(mockResponse.data);
expect(mockRawRequest).toHaveBeenCalled();
});
it('should handle team fetch errors', async () => {
mockRawRequest.mockRejectedValueOnce(new Error('Team fetch failed'));
await expect(graphqlClient.getTeams()).rejects.toThrow(
'GraphQL operation failed: Team fetch failed'
);
});
});
describe('getCurrentUser', () => {
it('should successfully fetch current user', async () => {
const mockResponse = {
data: {
viewer: {
id: 'user-1',
name: 'Test User',
email: 'test@example.com'
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const result: UserResponse = await graphqlClient.getCurrentUser();
expect(result).toEqual(mockResponse.data);
expect(mockRawRequest).toHaveBeenCalled();
});
it('should handle user fetch errors', async () => {
mockRawRequest.mockRejectedValueOnce(new Error('User fetch failed'));
await expect(graphqlClient.getCurrentUser()).rejects.toThrow(
'GraphQL operation failed: User fetch failed'
);
});
});
describe('Label Operations', () => {
it('should successfully create labels', async () => {
const mockResponse = {
data: {
labelCreate: {
success: true,
label: {
id: 'label-1',
name: 'bug'
}
}
}
};
mockRawRequest.mockResolvedValueOnce(mockResponse);
const labelInput: LabelInput = {
name: 'bug',
color: '#FF0000',
teamId: 'team-1'
};
const result: LabelResponse = await graphqlClient.createIssueLabels([labelInput]);
expect(result).toEqual(mockResponse.data);
expect(mockRawRequest).toHaveBeenCalled();
});
it('should handle label creation errors', async () => {
mockRawRequest.mockRejectedValueOnce(new Error('Label creation failed'));
const labelInput: LabelInput = {
name: 'bug',
teamId: 'team-1'
};
await expect(
graphqlClient.createIssueLabels([labelInput])
).rejects.toThrow('GraphQL operation failed: Label creation failed');
});
});
});