@atlassian-dc-mcp/bitbucket 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/build/__tests__/bitbucket-service.test.d.ts +2 -0
  3. package/build/__tests__/bitbucket-service.test.d.ts.map +1 -0
  4. package/build/__tests__/bitbucket-service.test.js +269 -0
  5. package/build/__tests__/bitbucket-service.test.js.map +1 -0
  6. package/build/__tests__/pr-changes-mapper.test.d.ts +2 -0
  7. package/build/__tests__/pr-changes-mapper.test.d.ts.map +1 -0
  8. package/build/__tests__/pr-changes-mapper.test.js +396 -0
  9. package/build/__tests__/pr-changes-mapper.test.js.map +1 -0
  10. package/build/__tests__/pr-comment-mapper.test.d.ts +2 -0
  11. package/build/__tests__/pr-comment-mapper.test.d.ts.map +1 -0
  12. package/build/__tests__/pr-comment-mapper.test.js +210 -0
  13. package/build/__tests__/pr-comment-mapper.test.js.map +1 -0
  14. package/build/bitbucket-service.d.ts +84 -1
  15. package/build/bitbucket-service.d.ts.map +1 -1
  16. package/build/bitbucket-service.js +148 -1
  17. package/build/bitbucket-service.js.map +1 -1
  18. package/build/index.js +13 -1
  19. package/build/index.js.map +1 -1
  20. package/build/pr-changes-mapper.d.ts +32 -0
  21. package/build/pr-changes-mapper.d.ts.map +1 -0
  22. package/build/pr-changes-mapper.js +111 -0
  23. package/build/pr-changes-mapper.js.map +1 -0
  24. package/build/pr-comment-mapper.d.ts +48 -0
  25. package/build/pr-comment-mapper.d.ts.map +1 -0
  26. package/build/pr-comment-mapper.js +128 -0
  27. package/build/pr-comment-mapper.js.map +1 -0
  28. package/jest.config.js +23 -0
  29. package/package.json +5 -3
  30. package/src/__tests__/bitbucket-service.test.ts +419 -0
  31. package/src/__tests__/pr-changes-mapper.test.ts +420 -0
  32. package/src/__tests__/pr-comment-mapper.test.ts +230 -0
  33. package/src/bitbucket-service.ts +215 -2
  34. package/src/index.ts +33 -2
  35. package/src/pr-changes-mapper.ts +216 -0
  36. package/src/pr-comment-mapper.ts +318 -0
  37. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,128 @@
1
+ // Type guard functions to validate object structure
2
+ function isBitbucketUser(obj) {
3
+ return (typeof obj === 'object' &&
4
+ obj !== null &&
5
+ typeof obj.name === 'string' &&
6
+ typeof obj.emailAddress === 'string' &&
7
+ typeof obj.active === 'boolean' &&
8
+ typeof obj.displayName === 'string' &&
9
+ typeof obj.id === 'number' &&
10
+ typeof obj.slug === 'string' &&
11
+ typeof obj.type === 'string' &&
12
+ typeof obj.links === 'object');
13
+ }
14
+ function isCommentAnchor(obj) {
15
+ return (typeof obj === 'object' &&
16
+ obj !== null &&
17
+ typeof obj.fromHash === 'string' &&
18
+ typeof obj.toHash === 'string' &&
19
+ typeof obj.line === 'number' &&
20
+ typeof obj.lineType === 'string' &&
21
+ typeof obj.fileType === 'string' &&
22
+ typeof obj.path === 'string' &&
23
+ typeof obj.diffType === 'string' &&
24
+ typeof obj.orphaned === 'boolean');
25
+ }
26
+ function isComment(obj) {
27
+ return (typeof obj === 'object' &&
28
+ obj !== null &&
29
+ typeof obj.id === 'number' &&
30
+ typeof obj.version === 'number' &&
31
+ typeof obj.text === 'string' &&
32
+ isBitbucketUser(obj.author) &&
33
+ typeof obj.createdDate === 'number' &&
34
+ typeof obj.updatedDate === 'number' &&
35
+ Array.isArray(obj.comments) &&
36
+ typeof obj.threadResolved === 'boolean' &&
37
+ typeof obj.severity === 'string' &&
38
+ typeof obj.state === 'string' &&
39
+ typeof obj.properties === 'object' &&
40
+ typeof obj.permittedOperations === 'object');
41
+ }
42
+ function isPRActivity(obj) {
43
+ return (typeof obj === 'object' &&
44
+ obj !== null &&
45
+ typeof obj.id === 'number' &&
46
+ typeof obj.createdDate === 'number' &&
47
+ isBitbucketUser(obj.user) &&
48
+ typeof obj.action === 'string');
49
+ }
50
+ function simplifyUser(user) {
51
+ return {
52
+ name: user.name,
53
+ displayName: user.displayName
54
+ };
55
+ }
56
+ function simplifyAnchor(anchor) {
57
+ return {
58
+ line: anchor.line,
59
+ path: anchor.path,
60
+ fileType: anchor.fileType
61
+ };
62
+ }
63
+ function simplifyComment(comment) {
64
+ return {
65
+ id: comment.id,
66
+ text: comment.text,
67
+ author: simplifyUser(comment.author),
68
+ createdDate: comment.createdDate,
69
+ ...(comment.anchor && { anchor: simplifyAnchor(comment.anchor) }),
70
+ threadResolved: comment.threadResolved,
71
+ state: comment.state
72
+ };
73
+ }
74
+ function simplifyActivity(activity) {
75
+ return {
76
+ id: activity.id,
77
+ createdDate: activity.createdDate,
78
+ user: simplifyUser(activity.user),
79
+ action: activity.action,
80
+ ...(activity.commentAction && { commentAction: activity.commentAction }),
81
+ ...(activity.comment && { comment: simplifyComment(activity.comment) })
82
+ };
83
+ }
84
+ export function simplifyBitbucketPRComments(response) {
85
+ const activities = [];
86
+ // Process each activity with type guard validation
87
+ for (const activity of response.values || []) {
88
+ if (isPRActivity(activity)) {
89
+ activities.push(simplifyActivity(activity));
90
+ }
91
+ // If type guard fails, we skip the invalid activity but continue processing
92
+ }
93
+ // If no valid activities were found, return the original response
94
+ if (activities.length === 0 && (response.values || []).length > 0) {
95
+ return response;
96
+ }
97
+ // Find PR author (usually the one who OPENED the PR)
98
+ const prAuthor = activities.find(a => a.action === 'OPENED')?.user;
99
+ // Count comments and unresolved threads
100
+ const comments = activities.filter(a => a.action === 'COMMENTED' && a.comment);
101
+ const unresolvedCount = comments.filter(a => a.comment && !a.comment.threadResolved).length;
102
+ return {
103
+ isLastPage: response.isLastPage ?? true,
104
+ activities,
105
+ summary: {
106
+ totalActivities: activities.length,
107
+ ...(prAuthor && { prAuthor }),
108
+ commentCount: comments.length,
109
+ unresolvedCount
110
+ }
111
+ };
112
+ }
113
+ export function getCommentSummary(response) {
114
+ const commentSummaries = [];
115
+ for (const activity of response.values || []) {
116
+ // Use type guard to validate activity structure
117
+ if (isPRActivity(activity) && activity.action === 'COMMENTED' && activity.comment) {
118
+ // Additional validation for comment structure
119
+ if (isComment(activity.comment)) {
120
+ const comment = activity.comment;
121
+ commentSummaries.push(`${comment.author.displayName} on ${comment.anchor?.path || 'PR'}:${comment.anchor?.line || ''}: ${comment.text}`);
122
+ }
123
+ }
124
+ // If type guard fails, we skip the invalid activity but continue processing
125
+ }
126
+ return commentSummaries;
127
+ }
128
+ //# sourceMappingURL=pr-comment-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pr-comment-mapper.js","sourceRoot":"","sources":["../src/pr-comment-mapper.ts"],"names":[],"mappings":"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"}
package/jest.config.js ADDED
@@ -0,0 +1,23 @@
1
+ export default {
2
+ preset: 'ts-jest/presets/default-esm',
3
+ extensionsToTreatAsEsm: ['.ts'],
4
+ moduleNameMapper: {
5
+ '^(\\.{1,2}/.*)\\.js$': '$1',
6
+ '^@atlassian-dc-mcp/common$': '<rootDir>/../common/src/index.ts',
7
+ },
8
+ transform: {
9
+ '^.+\\.ts$': ['ts-jest', {
10
+ useESM: true,
11
+ }],
12
+ },
13
+ transformIgnorePatterns: [
14
+ 'node_modules/(?!(@atlassian-dc-mcp)/)',
15
+ ],
16
+ testEnvironment: 'node',
17
+ testMatch: ['**/__tests__/**/*.test.ts'],
18
+ collectCoverageFrom: [
19
+ 'src/**/*.ts',
20
+ '!src/**/*.d.ts',
21
+ '!src/**/index.ts',
22
+ ],
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlassian-dc-mcp/bitbucket",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "bin": "./bin/run.js",
@@ -14,7 +14,7 @@
14
14
  "test": "jest"
15
15
  },
16
16
  "dependencies": {
17
- "@atlassian-dc-mcp/common": "^0.6.0",
17
+ "@atlassian-dc-mcp/common": "^0.7.0",
18
18
  "@modelcontextprotocol/sdk": "^1.5.0",
19
19
  "dotenv": "^16.4.7",
20
20
  "node-fetch": "^3.3.2",
@@ -22,12 +22,14 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@modelcontextprotocol/inspector": "^0.4.1",
25
+ "@types/jest": "^30.0.0",
25
26
  "lerna": "^8.2.0",
26
27
  "nodemon": "^3.1.9",
28
+ "ts-jest": "^29.4.0",
27
29
  "ts-node": "^10.9.2"
28
30
  },
29
31
  "publishConfig": {
30
32
  "access": "public"
31
33
  },
32
- "gitHead": "ad8a950dea96c3d90c545bd92fce8f6566fbf31c"
34
+ "gitHead": "25617f5fb5da84c8bef2ed56fb6464b1a2c84262"
33
35
  }
@@ -0,0 +1,419 @@
1
+ import { BitbucketService } from '../bitbucket-service.js';
2
+ import { PullRequestsService } from '../bitbucket-client/index.js';
3
+
4
+ // Mock the request function
5
+ jest.mock('../bitbucket-client/core/request.js', () => ({
6
+ request: jest.fn()
7
+ }));
8
+
9
+ // Mock the PullRequestsService
10
+ jest.mock('../bitbucket-client/index.js', () => ({
11
+ PullRequestsService: {
12
+ streamRawDiff2: jest.fn(),
13
+ createComment2: jest.fn(),
14
+ streamChanges1: jest.fn()
15
+ },
16
+ OpenAPI: {
17
+ BASE: '',
18
+ TOKEN: '',
19
+ VERSION: ''
20
+ }
21
+ }));
22
+
23
+ describe('BitbucketService', () => {
24
+ let bitbucketService: BitbucketService;
25
+ const mockProjectKey = 'TEST';
26
+ const mockRepositorySlug = 'test-repo';
27
+ const mockPullRequestId = '123';
28
+
29
+ beforeEach(() => {
30
+ bitbucketService = new BitbucketService('test-host', 'test-token');
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ describe('getPullRequestChanges', () => {
35
+ it('should successfully get PR changes', async () => {
36
+ const mockChangesData = {
37
+ values: [
38
+ { path: { toString: 'file.txt' }, type: 'MODIFY' }
39
+ ],
40
+ size: 1,
41
+ isLastPage: true
42
+ };
43
+ (PullRequestsService.streamChanges1 as jest.Mock).mockResolvedValue(mockChangesData);
44
+
45
+ const result = await bitbucketService.getPullRequestChanges(
46
+ mockProjectKey,
47
+ mockRepositorySlug,
48
+ mockPullRequestId
49
+ );
50
+
51
+ expect(result.success).toBe(true);
52
+ expect(result.data).toBe(mockChangesData);
53
+ expect(PullRequestsService.streamChanges1).toHaveBeenCalledWith(
54
+ mockProjectKey,
55
+ mockPullRequestId,
56
+ mockRepositorySlug,
57
+ undefined,
58
+ undefined,
59
+ undefined,
60
+ undefined,
61
+ undefined,
62
+ 25
63
+ );
64
+ });
65
+
66
+ it('should successfully get PR changes with all parameters', async () => {
67
+ const mockChangesData = {
68
+ values: [
69
+ { path: { toString: 'file.txt' }, type: 'MODIFY' }
70
+ ],
71
+ size: 1,
72
+ isLastPage: true
73
+ };
74
+ (PullRequestsService.streamChanges1 as jest.Mock).mockResolvedValue(mockChangesData);
75
+
76
+ const result = await bitbucketService.getPullRequestChanges(
77
+ mockProjectKey,
78
+ mockRepositorySlug,
79
+ mockPullRequestId,
80
+ 'abc123',
81
+ 'RANGE',
82
+ 'def456',
83
+ 'true',
84
+ 0,
85
+ 50
86
+ );
87
+
88
+ expect(result.success).toBe(true);
89
+ expect(result.data).toBe(mockChangesData);
90
+ expect(PullRequestsService.streamChanges1).toHaveBeenCalledWith(
91
+ mockProjectKey,
92
+ mockPullRequestId,
93
+ mockRepositorySlug,
94
+ 'abc123',
95
+ 'RANGE',
96
+ 'def456',
97
+ 'true',
98
+ 0,
99
+ 50
100
+ );
101
+ });
102
+
103
+ it('should handle API errors gracefully', async () => {
104
+ const mockError = new Error('API Error');
105
+ (PullRequestsService.streamChanges1 as jest.Mock).mockRejectedValue(mockError);
106
+
107
+ const result = await bitbucketService.getPullRequestChanges(
108
+ mockProjectKey,
109
+ mockRepositorySlug,
110
+ mockPullRequestId
111
+ );
112
+
113
+ expect(result.success).toBe(false);
114
+ expect(result.error).toBe('API Error');
115
+ });
116
+ });
117
+
118
+ describe('postPullRequestComment', () => {
119
+ it('should successfully post a general PR comment', async () => {
120
+ const mockComment = {
121
+ id: 12345,
122
+ text: 'Test comment',
123
+ author: { displayName: 'Test User' }
124
+ };
125
+ (PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
126
+
127
+ const result = await bitbucketService.postPullRequestComment(
128
+ mockProjectKey,
129
+ mockRepositorySlug,
130
+ mockPullRequestId,
131
+ 'Test comment'
132
+ );
133
+
134
+ expect(result.success).toBe(true);
135
+ expect(result.data).toBe(mockComment);
136
+ expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
137
+ mockProjectKey,
138
+ mockPullRequestId,
139
+ mockRepositorySlug,
140
+ { text: 'Test comment' }
141
+ );
142
+ });
143
+
144
+ it('should successfully post a reply comment', async () => {
145
+ const mockComment = {
146
+ id: 12346,
147
+ text: 'Reply comment',
148
+ author: { displayName: 'Test User' }
149
+ };
150
+ (PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
151
+
152
+ const result = await bitbucketService.postPullRequestComment(
153
+ mockProjectKey,
154
+ mockRepositorySlug,
155
+ mockPullRequestId,
156
+ 'Reply comment',
157
+ 123 // parentId
158
+ );
159
+
160
+ expect(result.success).toBe(true);
161
+ expect(result.data).toBe(mockComment);
162
+ expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
163
+ mockProjectKey,
164
+ mockPullRequestId,
165
+ mockRepositorySlug,
166
+ {
167
+ text: 'Reply comment',
168
+ parent: { id: 123 }
169
+ }
170
+ );
171
+ });
172
+
173
+ it('should successfully post a file comment', async () => {
174
+ const mockComment = {
175
+ id: 12347,
176
+ text: 'File comment',
177
+ author: { displayName: 'Test User' }
178
+ };
179
+ (PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
180
+
181
+ const result = await bitbucketService.postPullRequestComment(
182
+ mockProjectKey,
183
+ mockRepositorySlug,
184
+ mockPullRequestId,
185
+ 'File comment',
186
+ undefined, // parentId
187
+ 'src/test.js' // filePath
188
+ );
189
+
190
+ expect(result.success).toBe(true);
191
+ expect(result.data).toBe(mockComment);
192
+ expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
193
+ mockProjectKey,
194
+ mockPullRequestId,
195
+ mockRepositorySlug,
196
+ {
197
+ text: 'File comment',
198
+ anchor: {
199
+ path: 'src/test.js',
200
+ diffType: 'EFFECTIVE'
201
+ }
202
+ }
203
+ );
204
+ });
205
+
206
+ it('should successfully post a line comment', async () => {
207
+ const mockComment = {
208
+ id: 12348,
209
+ text: 'Line comment',
210
+ author: { displayName: 'Test User' }
211
+ };
212
+ (PullRequestsService.createComment2 as jest.Mock).mockResolvedValue(mockComment);
213
+
214
+ const result = await bitbucketService.postPullRequestComment(
215
+ mockProjectKey,
216
+ mockRepositorySlug,
217
+ mockPullRequestId,
218
+ 'Line comment',
219
+ undefined, // parentId
220
+ 'src/test.js', // filePath
221
+ 42, // line
222
+ 'ADDED' // lineType
223
+ );
224
+
225
+ expect(result.success).toBe(true);
226
+ expect(result.data).toBe(mockComment);
227
+ expect(PullRequestsService.createComment2).toHaveBeenCalledWith(
228
+ mockProjectKey,
229
+ mockPullRequestId,
230
+ mockRepositorySlug,
231
+ {
232
+ text: 'Line comment',
233
+ anchor: {
234
+ path: 'src/test.js',
235
+ diffType: 'EFFECTIVE',
236
+ line: 42,
237
+ lineType: 'ADDED',
238
+ fileType: 'TO'
239
+ }
240
+ }
241
+ );
242
+ });
243
+
244
+ it('should handle API errors gracefully', async () => {
245
+ const mockError = new Error('API Error');
246
+ (PullRequestsService.createComment2 as jest.Mock).mockRejectedValue(mockError);
247
+
248
+ const result = await bitbucketService.postPullRequestComment(
249
+ mockProjectKey,
250
+ mockRepositorySlug,
251
+ mockPullRequestId,
252
+ 'Test comment'
253
+ );
254
+
255
+ expect(result.success).toBe(false);
256
+ expect(result.error).toBe('API Error');
257
+ });
258
+ });
259
+
260
+ describe('getPullRequestDiff', () => {
261
+ const { request: mockRequest } = require('../bitbucket-client/core/request.js');
262
+
263
+ it('should successfully get raw diff with minimal parameters', async () => {
264
+ const mockRawDiff = 'diff --git a/file.txt b/file.txt\nindex 1234567..abcdefg 100644\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,4 @@\n line1\n line2\n+new line\n line3';
265
+ mockRequest.mockResolvedValue(mockRawDiff);
266
+
267
+ const result = await bitbucketService.getPullRequestDiff(
268
+ mockProjectKey,
269
+ mockRepositorySlug,
270
+ mockPullRequestId,
271
+ 'src/file.txt'
272
+ );
273
+
274
+ expect(result.success).toBe(true);
275
+ expect(result.data).toBe(mockRawDiff);
276
+ expect(mockRequest).toHaveBeenCalledWith(
277
+ expect.any(Object), // OpenAPI config
278
+ {
279
+ method: 'GET',
280
+ url: '/api/latest/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/diff/{path}',
281
+ path: {
282
+ 'path': 'src/file.txt',
283
+ 'projectKey': mockProjectKey,
284
+ 'pullRequestId': mockPullRequestId,
285
+ 'repositorySlug': mockRepositorySlug,
286
+ },
287
+ query: {
288
+ 'contextLines': undefined,
289
+ 'sinceId': undefined,
290
+ 'srcPath': undefined,
291
+ 'diffType': undefined,
292
+ 'untilId': undefined,
293
+ 'whitespace': undefined,
294
+ },
295
+ headers: {
296
+ 'Accept': 'text/plain'
297
+ },
298
+ errors: {
299
+ 400: `If the request was malformed.`,
300
+ 401: `The currently authenticated user has insufficient permissions to view the repository or pull request.`,
301
+ 404: `The repository or pull request does not exist.`,
302
+ },
303
+ }
304
+ );
305
+ });
306
+
307
+ it('should successfully get raw diff with all parameters', async () => {
308
+ const mockRawDiff = 'diff --git a/old/file.txt b/new/file.txt\nindex 1234567..abcdefg 100644\n--- a/old/file.txt\n+++ b/new/file.txt\n@@ -1,5 +1,6 @@\n line1\n line2\n+new line\n line3\n line4\n line5';
309
+ mockRequest.mockResolvedValue(mockRawDiff);
310
+
311
+ const result = await bitbucketService.getPullRequestDiff(
312
+ mockProjectKey,
313
+ mockRepositorySlug,
314
+ mockPullRequestId,
315
+ 'src/file.txt',
316
+ '5', // contextLines
317
+ 'abc123', // sinceId
318
+ 'old/file.txt', // srcPath
319
+ 'EFFECTIVE', // diffType
320
+ 'def456', // untilId
321
+ 'ignore-all' // whitespace
322
+ );
323
+
324
+ expect(result.success).toBe(true);
325
+ expect(result.data).toBe(mockRawDiff);
326
+ expect(mockRequest).toHaveBeenCalledWith(
327
+ expect.any(Object), // OpenAPI config
328
+ {
329
+ method: 'GET',
330
+ url: '/api/latest/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/diff/{path}',
331
+ path: {
332
+ 'path': 'src/file.txt',
333
+ 'projectKey': mockProjectKey,
334
+ 'pullRequestId': mockPullRequestId,
335
+ 'repositorySlug': mockRepositorySlug,
336
+ },
337
+ query: {
338
+ 'contextLines': '5',
339
+ 'sinceId': 'abc123',
340
+ 'srcPath': 'old/file.txt',
341
+ 'diffType': 'EFFECTIVE',
342
+ 'untilId': 'def456',
343
+ 'whitespace': 'ignore-all',
344
+ },
345
+ headers: {
346
+ 'Accept': 'text/plain'
347
+ },
348
+ errors: {
349
+ 400: `If the request was malformed.`,
350
+ 401: `The currently authenticated user has insufficient permissions to view the repository or pull request.`,
351
+ 404: `The repository or pull request does not exist.`,
352
+ },
353
+ }
354
+ );
355
+ });
356
+
357
+ it('should handle API errors gracefully', async () => {
358
+ const mockError = new Error('API Error');
359
+ mockRequest.mockRejectedValue(mockError);
360
+
361
+ const result = await bitbucketService.getPullRequestDiff(
362
+ mockProjectKey,
363
+ mockRepositorySlug,
364
+ mockPullRequestId,
365
+ 'src/file.txt'
366
+ );
367
+
368
+ expect(result.success).toBe(false);
369
+ expect(result.error).toBe('API Error');
370
+ });
371
+ });
372
+
373
+ describe('validateConfig', () => {
374
+ const originalEnv = process.env;
375
+
376
+ beforeEach(() => {
377
+ jest.resetModules();
378
+ process.env = { ...originalEnv };
379
+ });
380
+
381
+ afterAll(() => {
382
+ process.env = originalEnv;
383
+ });
384
+
385
+ it('should return empty array when all required env vars are present', () => {
386
+ process.env.BITBUCKET_API_TOKEN = 'test-token';
387
+ process.env.BITBUCKET_HOST = 'test-host';
388
+
389
+ const missingVars = BitbucketService.validateConfig();
390
+ expect(missingVars).toEqual([]);
391
+ });
392
+
393
+ it('should return missing vars when BITBUCKET_API_TOKEN is missing', () => {
394
+ delete process.env.BITBUCKET_API_TOKEN;
395
+ process.env.BITBUCKET_HOST = 'test-host';
396
+
397
+ const missingVars = BitbucketService.validateConfig();
398
+ expect(missingVars).toContain('BITBUCKET_API_TOKEN');
399
+ });
400
+
401
+ it('should return missing vars when both host options are missing', () => {
402
+ process.env.BITBUCKET_API_TOKEN = 'test-token';
403
+ delete process.env.BITBUCKET_HOST;
404
+ delete process.env.BITBUCKET_API_BASE_PATH;
405
+
406
+ const missingVars = BitbucketService.validateConfig();
407
+ expect(missingVars).toContain('BITBUCKET_HOST or BITBUCKET_API_BASE_PATH');
408
+ });
409
+
410
+ it('should accept BITBUCKET_API_BASE_PATH as alternative to BITBUCKET_HOST', () => {
411
+ process.env.BITBUCKET_API_TOKEN = 'test-token';
412
+ delete process.env.BITBUCKET_HOST;
413
+ process.env.BITBUCKET_API_BASE_PATH = 'https://test-host/rest';
414
+
415
+ const missingVars = BitbucketService.validateConfig();
416
+ expect(missingVars).toEqual([]);
417
+ });
418
+ });
419
+ });