408 lines
14 KiB
TypeScript
408 lines
14 KiB
TypeScript
/**
|
|
* Integration tests for Bitbucket MCP Server
|
|
* Tests against real Bitbucket API with iland-software-engineering/1111-frontend repo
|
|
*
|
|
* Requires BITBUCKET_MCP_EMAIL and BITBUCKET_MCP_TOKEN in .env or environment variable.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import { BitbucketClient } from '../../src/bitbucket-client.js';
|
|
import { BitbucketRouter, ToolResult } from '../../src/router.js';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as dotenv from 'dotenv';
|
|
|
|
const WORKSPACE = 'iland-software-engineering';
|
|
const REPO_SLUG = '1111-front';
|
|
|
|
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
|
|
const EMAIL = process.env.BITBUCKET_MCP_EMAIL || '';
|
|
const TOKEN = process.env.BITBUCKET_MCP_TOKEN || '';
|
|
const HAS_TOKEN = !!EMAIL && !!TOKEN && TOKEN.length >= 20;
|
|
|
|
describe('Bitbucket API Integration Tests', () => {
|
|
let client: BitbucketClient;
|
|
let router: BitbucketRouter;
|
|
|
|
beforeAll(() => {
|
|
client = new BitbucketClient();
|
|
router = new BitbucketRouter();
|
|
}, 35000);
|
|
|
|
describe('Pull Request Listing', () => {
|
|
it('should list pull requests', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
expect(Array.isArray(prs)).toBe(true);
|
|
if (prs.length > 0) {
|
|
expect(prs[0]).toHaveProperty('id');
|
|
expect(prs[0]).toHaveProperty('title');
|
|
expect(prs[0]).toHaveProperty('state');
|
|
}
|
|
});
|
|
|
|
it('should list open pull requests', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'open' });
|
|
expect(Array.isArray(prs)).toBe(true);
|
|
});
|
|
|
|
it('should list closed pull requests', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'closed' });
|
|
expect(Array.isArray(prs)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Pull Request Details', () => {
|
|
it('should get a specific pull request', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'open' });
|
|
|
|
if (prs.length === 0) {
|
|
const closedPrs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'closed' });
|
|
if (closedPrs.length === 0) {
|
|
console.log('⚠️ No PRs found in repository, skipping specific PR test');
|
|
return;
|
|
}
|
|
const pr = await client.getPullRequest(WORKSPACE, REPO_SLUG, closedPrs[0].id);
|
|
expect(pr).toHaveProperty('id');
|
|
expect(pr).toHaveProperty('title');
|
|
return;
|
|
}
|
|
|
|
const pr = await client.getPullRequest(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(pr).toHaveProperty('id');
|
|
expect(pr).toHaveProperty('title');
|
|
expect(pr.id).toBe(prs[0].id);
|
|
});
|
|
|
|
it('should get pull request status', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping status test');
|
|
return;
|
|
}
|
|
|
|
const status = await client.getPullRequestStatus(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(status).toHaveProperty('id');
|
|
expect(status).toHaveProperty('state');
|
|
expect(status).toHaveProperty('title');
|
|
});
|
|
|
|
it('should get pull request reviewers', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping reviewers test');
|
|
return;
|
|
}
|
|
|
|
const reviewers = await client.getPullRequestReviewers(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(Array.isArray(reviewers)).toBe(true);
|
|
});
|
|
|
|
it('should get pull request participants', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping participants test');
|
|
return;
|
|
}
|
|
|
|
const participants = await client.getPullRequestParticipants(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(participants).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Pull Request Activities & Comments', () => {
|
|
it('should get pull request activities', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping activities test');
|
|
return;
|
|
}
|
|
|
|
const activities = await client.getPullRequestActivities(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(activities).toBeDefined();
|
|
expect(activities).toHaveProperty('values');
|
|
});
|
|
|
|
it('should get pull request comments', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping comments test');
|
|
return;
|
|
}
|
|
|
|
const comments = await client.getPullRequestComments(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(comments).toBeDefined();
|
|
expect(comments).toHaveProperty('values');
|
|
});
|
|
|
|
it('should get pull request commits', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping commits test');
|
|
return;
|
|
}
|
|
|
|
const commits = await client.getPullRequestCommits(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(commits).toBeDefined();
|
|
expect(commits).toHaveProperty('values');
|
|
});
|
|
});
|
|
|
|
describe('Pull Request Changes & Diff', () => {
|
|
it('should get pull request changes', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping changes test');
|
|
return;
|
|
}
|
|
|
|
const changes = await client.getPullRequestChanges(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(changes).toBeDefined();
|
|
expect(changes).toHaveProperty('values');
|
|
});
|
|
|
|
it('should get pull request diff', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping diff test');
|
|
return;
|
|
}
|
|
|
|
const diff = await client.getPullRequestDiff(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(diff).toBeDefined();
|
|
});
|
|
|
|
it('should get pull request patch', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping patch test');
|
|
return;
|
|
}
|
|
|
|
const patch = await client.getPullRequestPatch(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(patch).toHaveProperty('patch');
|
|
expect(typeof patch.patch).toBe('string');
|
|
});
|
|
});
|
|
|
|
describe('Pull Request Tasks', () => {
|
|
it('should get pull request tasks', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping tasks test');
|
|
return;
|
|
}
|
|
|
|
const tasks = await client.getPullRequestTasks(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(tasks).toBeDefined();
|
|
});
|
|
|
|
it('should get pull request task count', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping task count test');
|
|
return;
|
|
}
|
|
|
|
const taskCount = await client.getPullRequestTaskCount(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(taskCount).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Full Pull Request', () => {
|
|
it('should get full pull request details', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG);
|
|
if (prs.length === 0) {
|
|
console.log('⚠️ No PRs found, skipping full PR test');
|
|
return;
|
|
}
|
|
|
|
const fullPR = await client.getFullPullRequest(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(fullPR).toHaveProperty('id');
|
|
expect(fullPR).toHaveProperty('title');
|
|
expect(fullPR).toHaveProperty('source');
|
|
expect(fullPR).toHaveProperty('destination');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Bitbucket Router Integration', () => {
|
|
let router: BitbucketRouter;
|
|
|
|
beforeAll(() => {
|
|
delete process.env.DEFAULT_WORKSPACE;
|
|
delete process.env.DEFAULT_REPO;
|
|
router = new BitbucketRouter();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
delete process.env.DEFAULT_WORKSPACE;
|
|
delete process.env.DEFAULT_REPO;
|
|
});
|
|
|
|
describe('Router parameter validation', () => {
|
|
it('should require workspace for list_pull_requests', async () => {
|
|
const result = await router.executeTool('list_pull_requests', {
|
|
repository: REPO_SLUG
|
|
}) as ToolResult;
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should require workspace and pullRequestId for get_pull_request', async () => {
|
|
const result = await router.executeTool('get_pull_request', {
|
|
workspace: WORKSPACE
|
|
}) as ToolResult;
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should handle unknown tool', async () => {
|
|
const result = await router.executeTool('unknown_tool', {}) as ToolResult;
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Unknown tool');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle invalid pull request ID', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
try {
|
|
await new BitbucketClient().getPullRequest(WORKSPACE, REPO_SLUG, 999999999);
|
|
} catch (error) {
|
|
expect(error).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
const RUN_WRITES = process.env.RUN_WRITE_TESTS === 'true';
|
|
|
|
describe('Repository / Workspace (read)', () => {
|
|
let client: BitbucketClient;
|
|
|
|
beforeAll(() => {
|
|
client = new BitbucketClient();
|
|
});
|
|
|
|
it('should list workspaces', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const result = await client.listWorkspaces({ pagelen: 10 });
|
|
expect(result).toHaveProperty('values');
|
|
expect(Array.isArray(result.values)).toBe(true);
|
|
});
|
|
|
|
it('should list repositories in workspace', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const result = await client.listRepositories(WORKSPACE, { pagelen: 10 });
|
|
expect(result).toHaveProperty('values');
|
|
expect(Array.isArray(result.values)).toBe(true);
|
|
});
|
|
|
|
it('should get a specific repository', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const result = await client.getRepository(WORKSPACE, REPO_SLUG);
|
|
expect(result).toHaveProperty('slug');
|
|
expect(result).toHaveProperty('full_name');
|
|
});
|
|
|
|
it('should list branches', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const result = await client.listBranches(WORKSPACE, REPO_SLUG, { pagelen: 10 });
|
|
expect(result).toHaveProperty('values');
|
|
expect(Array.isArray(result.values)).toBe(true);
|
|
if (result.values.length > 0) {
|
|
expect(result.values[0]).toHaveProperty('name');
|
|
}
|
|
});
|
|
|
|
it('should filter branches by name', async () => {
|
|
if (!HAS_TOKEN) return;
|
|
const result = await client.listBranches(WORKSPACE, REPO_SLUG, { filter_by_name: 'main' });
|
|
expect(result).toHaveProperty('values');
|
|
});
|
|
});
|
|
|
|
describe('PR Write Operations (requires RUN_WRITE_TESTS=true)', () => {
|
|
let client: BitbucketClient;
|
|
|
|
beforeAll(() => {
|
|
client = new BitbucketClient();
|
|
});
|
|
|
|
it('should approve a PR', async () => {
|
|
if (!HAS_TOKEN || !RUN_WRITES) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
|
|
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
|
|
const result = await client.approvePullRequest(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
it('should unapprove a PR', async () => {
|
|
if (!HAS_TOKEN || !RUN_WRITES) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
|
|
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
|
|
const result = await client.unapprovePullRequest(WORKSPACE, REPO_SLUG, prs[0].id);
|
|
expect(result).toHaveProperty('success', true);
|
|
});
|
|
});
|
|
|
|
describe('Comment / Task Write Operations (requires RUN_WRITE_TESTS=true)', () => {
|
|
let client: BitbucketClient;
|
|
|
|
beforeAll(() => {
|
|
client = new BitbucketClient();
|
|
});
|
|
|
|
it('should add, update, then delete a comment', async () => {
|
|
if (!HAS_TOKEN || !RUN_WRITES) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
|
|
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
|
|
const prId = prs[0].id;
|
|
|
|
const added = await client.addPullRequestComment(WORKSPACE, REPO_SLUG, prId, { content: 'Integration test comment' });
|
|
expect(added).toHaveProperty('id');
|
|
const commentId = added.id;
|
|
|
|
const updated = await client.updatePullRequestComment(WORKSPACE, REPO_SLUG, prId, commentId, { content: 'Updated comment' });
|
|
expect(updated).toHaveProperty('id', commentId);
|
|
|
|
const deleted = await client.deletePullRequestComment(WORKSPACE, REPO_SLUG, prId, commentId);
|
|
expect(deleted).toHaveProperty('success', true);
|
|
});
|
|
|
|
it('should create, resolve, then delete a task', async () => {
|
|
if (!HAS_TOKEN || !RUN_WRITES) return;
|
|
const prs = await client.listPullRequests(WORKSPACE, REPO_SLUG, { state: 'OPEN' });
|
|
if (prs.length === 0) { console.log('No open PRs, skipping'); return; }
|
|
const prId = prs[0].id;
|
|
|
|
const created = await client.createPullRequestTask(WORKSPACE, REPO_SLUG, prId, { content: 'Integration test task' });
|
|
expect(created).toHaveProperty('id');
|
|
const taskId = created.id;
|
|
|
|
const resolved = await client.updatePullRequestTask(WORKSPACE, REPO_SLUG, prId, taskId, { state: 'RESOLVED' });
|
|
expect(resolved).toBeDefined();
|
|
|
|
const deleted = await client.deletePullRequestTask(WORKSPACE, REPO_SLUG, prId, taskId);
|
|
expect(deleted).toHaveProperty('success', true);
|
|
});
|
|
});
|