/** * 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'; const TEST_PR_ID = process.env.TEST_PR_ID ? parseInt(process.env.TEST_PR_ID, 10) : undefined; 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 and TEST_PR_ID)', () => { let client: BitbucketClient; beforeAll(() => { client = new BitbucketClient(); }); it('should approve then immediately unapprove a PR', async () => { if (!HAS_TOKEN || !RUN_WRITES) return; if (!TEST_PR_ID) { console.log('⚠️ TEST_PR_ID not set, skipping approve/unapprove test'); return; } await client.approvePullRequest(WORKSPACE, REPO_SLUG, TEST_PR_ID); try { const unapproved = await client.unapprovePullRequest(WORKSPACE, REPO_SLUG, TEST_PR_ID); expect(unapproved).toHaveProperty('success', true); } finally { await client.unapprovePullRequest(WORKSPACE, REPO_SLUG, TEST_PR_ID).catch(() => {}); } }); }); describe('Comment / Task Write Operations (requires RUN_WRITE_TESTS=true and TEST_PR_ID)', () => { let client: BitbucketClient; beforeAll(() => { client = new BitbucketClient(); }); it('should add, update, then delete a comment', async () => { if (!HAS_TOKEN || !RUN_WRITES) return; if (!TEST_PR_ID) { console.log('⚠️ TEST_PR_ID not set, skipping comment test'); return; } const added = await client.addPullRequestComment(WORKSPACE, REPO_SLUG, TEST_PR_ID, { content: 'Integration test comment' }); expect(added).toHaveProperty('id'); const commentId = added.id; try { const updated = await client.updatePullRequestComment(WORKSPACE, REPO_SLUG, TEST_PR_ID, commentId, { content: 'Updated comment' }); expect(updated).toHaveProperty('id', commentId); } finally { await client.deletePullRequestComment(WORKSPACE, REPO_SLUG, TEST_PR_ID, commentId).catch(() => {}); } }); it('should create, resolve, then delete a task', async () => { if (!HAS_TOKEN || !RUN_WRITES) return; if (!TEST_PR_ID) { console.log('⚠️ TEST_PR_ID not set, skipping task test'); return; } const created = await client.createPullRequestTask(WORKSPACE, REPO_SLUG, TEST_PR_ID, { content: 'Integration test task' }); expect(created).toHaveProperty('id'); const taskId = created.id; try { const resolved = await client.updatePullRequestTask(WORKSPACE, REPO_SLUG, TEST_PR_ID, taskId, { state: 'RESOLVED' }); expect(resolved).toBeDefined(); } finally { await client.deletePullRequestTask(WORKSPACE, REPO_SLUG, TEST_PR_ID, taskId).catch(() => {}); } }); });