diff --git a/README.md b/README.md index af635e6..854e744 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,13 @@ npm link # Bearer authentication (recommended) export JIRA_HOST=your-jira-instance.atlassian.net export JIRA_API_TOKEN=your-api-token + export JIRA_API_VERSION=auto # optional: auto (default), 2, 3 # Basic authentication (optional) export JIRA_HOST=your-jira-instance.atlassian.net export JIRA_API_TOKEN=your-api-token export JIRA_USERNAME=your-email@company.com + export JIRA_API_VERSION=auto # optional: auto (default), 2, 3 ``` 4. **Verify connection:** @@ -101,11 +103,20 @@ jira config --server https://yourcompany.atlassian.net \ jira config set server https://yourcompany.atlassian.net jira config set token your-api-token jira config set username your-email@company.com # optional +jira config set apiVersion auto # optional: auto (default), 2, 3 # Show current configuration jira config --show ``` +### Jira REST API Version + +By default, the CLI uses `auto` mode: it tries Jira REST API v3 first and automatically retries with v2 if needed. If a fallback happens, the CLI keeps using the working version for the rest of the process. + +You can override the behavior: +- Config: `jira config set apiVersion auto|2|3` +- Env: `JIRA_API_VERSION=auto|2|3` + ### Option 2: Environment Variables You can configure the CLI using environment variables in either a new or legacy format: @@ -115,11 +126,13 @@ You can configure the CLI using environment variables in either a new or legacy # Bearer authentication export JIRA_HOST="your-jira-instance.atlassian.net" export JIRA_API_TOKEN="your-api-token" +export JIRA_API_VERSION="auto" # optional: auto (default), 2, 3 # Basic authentication (add username) export JIRA_HOST="your-jira-instance.atlassian.net" export JIRA_API_TOKEN="your-api-token" export JIRA_USERNAME="your-email@company.com" +export JIRA_API_VERSION="auto" # optional: auto (default), 2, 3 ``` #### Legacy format (JIRA_DOMAIN) @@ -127,6 +140,7 @@ export JIRA_USERNAME="your-email@company.com" export JIRA_DOMAIN="your-domain.atlassian.net" export JIRA_USERNAME="your-email@company.com" export JIRA_API_TOKEN="your-api-token" +export JIRA_API_VERSION="auto" # optional: auto (default), 2, 3 ``` ### Getting Your API Token diff --git a/docs/API.md b/docs/API.md index ff85eb7..69b6be0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,7 +15,9 @@ const client = new JiraClient(config) ``` **Parameters:** -- `config` (Object): Configuration object containing server, username, and token +- `config` (Object): Configuration object containing server, username, token, and optional apiVersion (`auto`, `2`, `3`) + +When `apiVersion` is `auto`, the client starts with v3 and retries with v2 on certain endpoint failures; the successful version is kept for the rest of the process. #### Methods diff --git a/lib/config.js b/lib/config.js index 077dde3..823e283 100644 --- a/lib/config.js +++ b/lib/config.js @@ -16,6 +16,11 @@ class Config { }, token: { type: 'string' + }, + apiVersion: { + type: 'string', + enum: ['auto', '2', '3'], + default: 'auto' } } }); @@ -62,7 +67,8 @@ class Config { process.env.JIRA_HOST : `https://${process.env.JIRA_HOST}`, username: process.env.JIRA_USERNAME || '', // Empty username for token-only auth - token: process.env.JIRA_API_TOKEN + token: process.env.JIRA_API_TOKEN, + apiVersion: process.env.JIRA_API_VERSION || this.get('apiVersion') || 'auto' }; } @@ -73,7 +79,8 @@ class Config { process.env.JIRA_DOMAIN : `https://${process.env.JIRA_DOMAIN}`, username: process.env.JIRA_USERNAME, - token: process.env.JIRA_API_TOKEN + token: process.env.JIRA_API_TOKEN, + apiVersion: process.env.JIRA_API_VERSION || this.get('apiVersion') || 'auto' }; } @@ -93,7 +100,8 @@ class Config { return { server: this.get('server'), username: this.get('username') || '', - token: this.get('token') + token: this.get('token'), + apiVersion: process.env.JIRA_API_VERSION || this.get('apiVersion') || 'auto' }; } @@ -137,10 +145,12 @@ class Config { console.log('Server:', chalk.green(process.env.JIRA_HOST)); console.log('Username:', chalk.green(process.env.JIRA_USERNAME || '(token auth)')); console.log('Token:', chalk.green('***configured***')); + console.log('API Version:', chalk.green(process.env.JIRA_API_VERSION || 'auto')); } else if (process.env.JIRA_DOMAIN) { console.log('Server:', chalk.green(process.env.JIRA_DOMAIN)); console.log('Username:', chalk.green(process.env.JIRA_USERNAME)); console.log('Token:', chalk.green('***configured***')); + console.log('API Version:', chalk.green(process.env.JIRA_API_VERSION || 'auto')); } } @@ -149,6 +159,7 @@ class Config { console.log('Server:', chalk.green(config.server || 'Not set')); console.log('Username:', chalk.green(config.username || '(Bearer auth)')); console.log('Token:', config.token ? chalk.green('Set (hidden)') : chalk.red('Not set')); + console.log('API Version:', chalk.green(config.apiVersion || 'auto')); } if (this.isConfigured()) { @@ -160,4 +171,4 @@ class Config { } -module.exports = Config; \ No newline at end of file +module.exports = Config; diff --git a/lib/jira-client.js b/lib/jira-client.js index e70ef5f..1824711 100644 --- a/lib/jira-client.js +++ b/lib/jira-client.js @@ -4,7 +4,40 @@ class JiraClient { constructor(config) { this.config = config; this.baseURL = config.server; - + this.apiVersionMode = this.normalizeApiVersionMode(config.apiVersion || process.env.JIRA_API_VERSION); + this.apiVersion = this.apiVersionMode === 'auto' ? 3 : this.apiVersionMode; + this.axiosConfigKeys = new Set([ + 'adapter', + 'auth', + 'baseURL', + 'data', + 'decompress', + 'headers', + 'httpAgent', + 'httpsAgent', + 'maxBodyLength', + 'maxContentLength', + 'maxRedirects', + 'onDownloadProgress', + 'onUploadProgress', + 'params', + 'paramsSerializer', + 'proxy', + 'responseEncoding', + 'responseType', + 'signal', + 'timeout', + 'timeoutErrorMessage', + 'transformRequest', + 'transformResponse', + 'transitional', + 'url', + 'validateStatus', + 'withCredentials', + 'xsrfCookieName', + 'xsrfHeaderName' + ]); + // Support both token and basic auth const headers = { 'Content-Type': 'application/json', @@ -24,41 +57,15 @@ class JiraClient { }; } - this.client = axios.create({ - baseURL: `${this.baseURL}/rest/api/2`, - auth: auth, - headers: headers - }); - - // Add response interceptor for error handling - this.client.interceptors.response.use( - response => response, - error => { - if (error.response) { - const { status, data } = error.response; - switch (status) { - case 401: - throw new Error('Authentication failed. Please check your credentials.'); - case 403: - throw new Error('Access denied. You don\'t have permission to perform this action.'); - case 404: - throw new Error('Resource not found.'); - default: - throw new Error(data.errorMessages ? data.errorMessages.join(', ') : 'API request failed'); - } - } else if (error.request) { - throw new Error('Network error. Please check your connection and server URL.'); - } else { - throw new Error(error.message); - } - } - ); + this.clientV2 = this.createApiClient(2, { auth, headers }); + this.clientV3 = this.createApiClient(3, { auth, headers }); + this.agileClient = this.createAgileClient({ auth, headers }); } // Test connection async testConnection() { try { - const response = await this.client.get('/myself'); + const response = await this.requestApi('get', '/myself'); return { success: true, user: response.data @@ -73,7 +80,7 @@ class JiraClient { // Issues async getIssue(issueKey) { - const response = await this.client.get(`/issue/${issueKey}`); + const response = await this.requestApi('get', `/issue/${issueKey}`); return response.data; } @@ -84,75 +91,81 @@ class JiraClient { maxResults: options.maxResults || 50, fields: options.fields || ['summary', 'status', 'assignee', 'created', 'updated'] }; - - const response = await this.client.get('/search', { params }); + + const { response } = await this.requestSearch({ params }); return response.data; } async createIssue(issueData) { - const response = await this.client.post('/issue', issueData); + const response = await this.requestApi('post', '/issue', issueData); return response.data; } async updateIssue(issueKey, updateData) { - const response = await this.client.put(`/issue/${issueKey}`, updateData); + const response = await this.requestApi('put', `/issue/${issueKey}`, updateData); return response.data; } async deleteIssue(issueKey) { - await this.client.delete(`/issue/${issueKey}`); + await this.requestApi('delete', `/issue/${issueKey}`); return true; } // Projects async getProjects() { - const response = await this.client.get('/project'); + const response = await this.requestApi('get', '/project'); return response.data; } async getProject(projectKey) { - const response = await this.client.get(`/project/${projectKey}`); + const response = await this.requestApi('get', `/project/${projectKey}`); return response.data; } // Sprints (requires Agile API) async getSprints(boardId) { - const response = await this.client.get(`/board/${boardId}/sprint`, { - baseURL: `${this.baseURL}/rest/agile/1.0` - }); + const response = await this.agileClient.get(`/board/${boardId}/sprint`); return response.data; } async getBoards() { - const response = await this.client.get('/board', { - baseURL: `${this.baseURL}/rest/agile/1.0` - }); + const response = await this.agileClient.get('/board'); return response.data; } // Issue types async getIssueTypes() { - const response = await this.client.get('/issuetype'); + const response = await this.requestApi('get', '/issuetype'); return response.data; } // Statuses async getStatuses() { - const response = await this.client.get('/status'); + const response = await this.requestApi('get', '/status'); return response.data; } // Users async searchUsers(query) { - const response = await this.client.get('/user/search', { - params: { username: query } - }); - return response.data; + const preferred = this.apiVersion; + const paramsByVersion = version => (version === 3 ? { query } : { username: query }); + + try { + const response = await this.getApiClient(preferred).get('/user/search', { params: paramsByVersion(preferred) }); + return response.data; + } catch (error) { + if (this.apiVersionMode !== 'auto' || !this.shouldFallbackApiVersion(error)) throw error; + + const fallbackVersion = preferred === 3 ? 2 : 3; + const response = await this.getApiClient(fallbackVersion).get('/user/search', { params: paramsByVersion(fallbackVersion) }); + this.apiVersion = fallbackVersion; + return response.data; + } } // Comments async getComments(issueKey) { - const response = await this.client.get(`/issue/${issueKey}/comment`); + const response = await this.requestApi('get', `/issue/${issueKey}/comment`); return response.data; } @@ -169,7 +182,7 @@ class JiraClient { }; } - const response = await this.client.post(`/issue/${issueKey}/comment`, commentData); + const response = await this.requestApi('post', `/issue/${issueKey}/comment`, commentData); return response.data; } @@ -178,14 +191,140 @@ class JiraClient { body: body }; - const response = await this.client.put(`/comment/${commentId}`, commentData); + const response = await this.requestApi('put', `/comment/${commentId}`, commentData); return response.data; } async deleteComment(commentId) { - await this.client.delete(`/comment/${commentId}`); + await this.requestApi('delete', `/comment/${commentId}`); return true; } + + normalizeApiVersionMode(apiVersion) { + if (!apiVersion) return 'auto'; + if (apiVersion === 'auto') return 'auto'; + if (apiVersion === 2 || apiVersion === '2') return 2; + if (apiVersion === 3 || apiVersion === '3') return 3; + return 'auto'; + } + + createApiClient(version, { auth, headers }) { + const client = axios.create({ + baseURL: `${this.baseURL}/rest/api/${version}`, + auth, + headers + }); + client.interceptors.response.use( + response => response, + error => Promise.reject(this.toJiraError(error)) + ); + return client; + } + + createAgileClient({ auth, headers }) { + const client = axios.create({ + baseURL: `${this.baseURL}/rest/agile/1.0`, + auth, + headers + }); + client.interceptors.response.use( + response => response, + error => Promise.reject(this.toJiraError(error)) + ); + return client; + } + + toJiraError(error) { + if (error.response) { + const { status, data } = error.response; + + const jiraError = new Error(this.formatJiraErrorMessage(status, data)); + jiraError.status = status; + jiraError.data = data; + jiraError.method = error.config?.method; + jiraError.url = error.config?.url; + return jiraError; + } + + if (error.request) { + const jiraError = new Error('Network error. Please check your connection and server URL.'); + jiraError.request = error.request; + return jiraError; + } + + return error instanceof Error ? error : new Error(String(error)); + } + + formatJiraErrorMessage(status, data) { + if (status === 401) return 'Authentication failed. Please check your credentials.'; + if (status === 403) return 'Access denied. You don\'t have permission to perform this action.'; + if (status === 404) return 'Resource not found.'; + return data?.errorMessages ? data.errorMessages.join(', ') : 'API request failed'; + } + + getApiClient(version) { + return version === 2 ? this.clientV2 : this.clientV3; + } + + shouldFallbackApiVersion(error) { + if (!error || typeof error !== 'object') return false; + if (error.status === 404 || error.status === 410) return true; + if (typeof error.message === 'string' && error.message.includes('requested API has been removed')) return true; + if (typeof error.message === 'string' && error.message.includes('Please migrate to')) return true; + if (typeof error.message === 'string' && error.message.includes('/rest/api/')) return true; + return false; + } + + async requestApi(method, url, dataOrConfig) { + const axiosConfig = this.normalizeAxiosArgs(method, dataOrConfig); + const preferred = this.apiVersion; + + try { + return await this.getApiClient(preferred).request({ method, url, ...axiosConfig }); + } catch (error) { + if (this.apiVersionMode !== 'auto' || !this.shouldFallbackApiVersion(error)) throw error; + + const fallbackVersion = preferred === 3 ? 2 : 3; + const response = await this.getApiClient(fallbackVersion).request({ method, url, ...axiosConfig }); + this.apiVersion = fallbackVersion; + return response; + } + } + + async requestSearch({ params }) { + const preferred = this.apiVersion; + const urlByVersion = version => (version === 3 ? '/search/jql' : '/search'); + + try { + const response = await this.getApiClient(preferred).get(urlByVersion(preferred), { params }); + return { apiVersion: preferred, response }; + } catch (error) { + if (this.apiVersionMode !== 'auto' || !this.shouldFallbackApiVersion(error)) throw error; + + const fallbackVersion = preferred === 3 ? 2 : 3; + const response = await this.getApiClient(fallbackVersion).get(urlByVersion(fallbackVersion), { params }); + this.apiVersion = fallbackVersion; + return { apiVersion: fallbackVersion, response }; + } + } + + normalizeAxiosArgs(method, dataOrConfig) { + const lowerMethod = method.toLowerCase(); + const expectsBody = ['post', 'put', 'patch'].includes(lowerMethod); + + if (!expectsBody) { + return dataOrConfig && typeof dataOrConfig === 'object' ? dataOrConfig : {}; + } + + const hasConfigShape = + dataOrConfig && + typeof dataOrConfig === 'object' && + !Array.isArray(dataOrConfig) && + Object.keys(dataOrConfig).some(key => this.axiosConfigKeys.has(key)); + + if (hasConfigShape) return dataOrConfig; + return { data: dataOrConfig }; + } } -module.exports = JiraClient; \ No newline at end of file +module.exports = JiraClient; diff --git a/lib/utils.js b/lib/utils.js index 92a2a2f..6d06617 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -128,7 +128,7 @@ function formatIssueAsMarkdown(issue) { lines.push(`- **Labels**: ${issue.fields.labels.join(', ')}`); } - const url = issue.self.replace(`/rest/api/2/issue/${issue.id}`, `/browse/${issue.key}`); + const url = issue.self.replace(new RegExp(`/rest/api/(2|3)/issue/${issue.id}`), `/browse/${issue.key}`); lines.push(`- **URL**: ${url}`); lines.push(''); @@ -164,7 +164,7 @@ function displayIssueDetails(issue) { console.log(`\n${chalk.bold('Labels:')} ${issue.fields.labels.join(', ')}`); } - console.log(`\n${chalk.bold('URL:')} ${issue.self.replace('/rest/api/2/issue/' + issue.id, '/browse/' + issue.key)}`); + console.log(`\n${chalk.bold('URL:')} ${issue.self.replace(new RegExp(`/rest/api/(2|3)/issue/${issue.id}`), '/browse/' + issue.key)}`); } // Build JQL query from options diff --git a/tests/jira-client.test.js b/tests/jira-client.test.js index 8f6c01e..fc0f8f6 100644 --- a/tests/jira-client.test.js +++ b/tests/jira-client.test.js @@ -18,15 +18,19 @@ describe('JiraClient', () => { test('should create client with correct config', () => { expect(client.config).toEqual(mockConfig); expect(client.baseURL).toBe(mockConfig.server); - // Auth is now handled internally by axios client - expect(client.client.defaults.auth).toEqual({ + expect(client.clientV2.defaults.auth).toEqual({ + username: mockConfig.username, + password: mockConfig.token + }); + expect(client.clientV3.defaults.auth).toEqual({ username: mockConfig.username, password: mockConfig.token }); }); - test('should set up axios client with correct base URL', () => { - expect(client.client.defaults.baseURL).toBe(`${mockConfig.server}/rest/api/2`); + test('should set up axios clients with correct base URL', () => { + expect(client.clientV2.defaults.baseURL).toBe(`${mockConfig.server}/rest/api/2`); + expect(client.clientV3.defaults.baseURL).toBe(`${mockConfig.server}/rest/api/3`); }); test('should create client with Bearer auth when username is empty', () => { @@ -40,8 +44,10 @@ describe('JiraClient', () => { expect(bearerClient.config).toEqual(bearerConfig); expect(bearerClient.baseURL).toBe(bearerConfig.server); - expect(bearerClient.client.defaults.auth).toBeNull(); - expect(bearerClient.client.defaults.headers['Authorization']).toBe('Bearer test-token'); + expect(bearerClient.clientV2.defaults.auth).toBeNull(); + expect(bearerClient.clientV3.defaults.auth).toBeNull(); + expect(bearerClient.clientV2.defaults.headers['Authorization']).toBe('Bearer test-token'); + expect(bearerClient.clientV3.defaults.headers['Authorization']).toBe('Bearer test-token'); }); test('should create client with Bearer auth when username is missing', () => { @@ -52,8 +58,10 @@ describe('JiraClient', () => { const bearerClient = new JiraClient(bearerConfig); - expect(bearerClient.client.defaults.auth).toBeNull(); - expect(bearerClient.client.defaults.headers['Authorization']).toBe('Bearer test-token'); + expect(bearerClient.clientV2.defaults.auth).toBeNull(); + expect(bearerClient.clientV3.defaults.auth).toBeNull(); + expect(bearerClient.clientV2.defaults.headers['Authorization']).toBe('Bearer test-token'); + expect(bearerClient.clientV3.defaults.headers['Authorization']).toBe('Bearer test-token'); }); test('should create client with Basic auth when username is provided', () => { @@ -65,40 +73,43 @@ describe('JiraClient', () => { const basicClient = new JiraClient(basicConfig); - expect(basicClient.client.defaults.auth).toEqual({ + expect(basicClient.clientV2.defaults.auth).toEqual({ username: basicConfig.username, password: basicConfig.token }); - expect(basicClient.client.defaults.headers['Authorization']).toBeUndefined(); + expect(basicClient.clientV3.defaults.auth).toEqual({ + username: basicConfig.username, + password: basicConfig.token + }); + expect(basicClient.clientV2.defaults.headers['Authorization']).toBeUndefined(); + expect(basicClient.clientV3.defaults.headers['Authorization']).toBeUndefined(); }); }); describe('API methods', () => { // Mock axios client beforeEach(() => { - client.client.get = jest.fn(); - client.client.post = jest.fn(); - client.client.put = jest.fn(); - client.client.delete = jest.fn(); + client.clientV3.request = jest.fn(); + client.clientV3.get = jest.fn(); }); test('getIssue should make correct API call', async () => { const mockIssue = { key: 'TEST-1', fields: { summary: 'Test Issue' } }; - client.client.get.mockResolvedValue({ data: mockIssue }); + client.clientV3.request.mockResolvedValue({ data: mockIssue }); const result = await client.getIssue('TEST-1'); - expect(client.client.get).toHaveBeenCalledWith('/issue/TEST-1'); + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'get', url: '/issue/TEST-1' }); expect(result).toEqual(mockIssue); }); test('searchIssues should make correct API call with JQL', async () => { const mockSearch = { issues: [], total: 0 }; - client.client.get.mockResolvedValue({ data: mockSearch }); + client.clientV3.get.mockResolvedValue({ data: mockSearch }); const result = await client.searchIssues('project = TEST'); - expect(client.client.get).toHaveBeenCalledWith('/search', { + expect(client.clientV3.get).toHaveBeenCalledWith('/search/jql', { params: { jql: 'project = TEST', startAt: 0, @@ -119,21 +130,21 @@ describe('JiraClient', () => { } }; - client.client.post.mockResolvedValue({ data: mockResponse }); + client.clientV3.request.mockResolvedValue({ data: mockResponse }); const result = await client.createIssue(issueData); - expect(client.client.post).toHaveBeenCalledWith('/issue', issueData); + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'post', url: '/issue', data: issueData }); expect(result).toEqual(mockResponse); }); test('getProjects should make correct API call', async () => { const mockProjects = [{ key: 'TEST', name: 'Test Project' }]; - client.client.get.mockResolvedValue({ data: mockProjects }); + client.clientV3.request.mockResolvedValue({ data: mockProjects }); const result = await client.getProjects(); - expect(client.client.get).toHaveBeenCalledWith('/project'); + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'get', url: '/project' }); expect(result).toEqual(mockProjects); }); @@ -143,60 +154,56 @@ describe('JiraClient', () => { { id: '10000', body: 'Test comment', author: { displayName: 'Test User' } } ] }; - client.client.get.mockResolvedValue({ data: mockComments }); + client.clientV3.request.mockResolvedValue({ data: mockComments }); const result = await client.getComments('TEST-1'); - expect(client.client.get).toHaveBeenCalledWith('/issue/TEST-1/comment'); + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'get', url: '/issue/TEST-1/comment' }); expect(result).toEqual(mockComments); }); test('addComment should make correct API call', async () => { const mockComment = { id: '10001', body: 'New comment' }; - client.client.post.mockResolvedValue({ data: mockComment }); + client.clientV3.request.mockResolvedValue({ data: mockComment }); const result = await client.addComment('TEST-1', 'New comment'); - expect(client.client.post).toHaveBeenCalledWith('/issue/TEST-1/comment', { - body: 'New comment' - }); + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'post', url: '/issue/TEST-1/comment', data: { body: 'New comment' } }); expect(result).toEqual(mockComment); }); test('addComment with internal flag should include visibility', async () => { const mockComment = { id: '10001', body: 'Internal comment' }; - client.client.post.mockResolvedValue({ data: mockComment }); + client.clientV3.request.mockResolvedValue({ data: mockComment }); const result = await client.addComment('TEST-1', 'Internal comment', { internal: true }); - expect(client.client.post).toHaveBeenCalledWith('/issue/TEST-1/comment', { + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'post', url: '/issue/TEST-1/comment', data: { body: 'Internal comment', visibility: { type: 'role', value: 'Administrators' } - }); + } }); expect(result).toEqual(mockComment); }); test('updateComment should make correct API call', async () => { const mockComment = { id: '10000', body: 'Updated comment' }; - client.client.put.mockResolvedValue({ data: mockComment }); + client.clientV3.request.mockResolvedValue({ data: mockComment }); const result = await client.updateComment('10000', 'Updated comment'); - expect(client.client.put).toHaveBeenCalledWith('/comment/10000', { - body: 'Updated comment' - }); + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'put', url: '/comment/10000', data: { body: 'Updated comment' } }); expect(result).toEqual(mockComment); }); test('deleteComment should make correct API call', async () => { - client.client.delete.mockResolvedValue({}); + client.clientV3.request.mockResolvedValue({}); const result = await client.deleteComment('10000'); - expect(client.client.delete).toHaveBeenCalledWith('/comment/10000'); + expect(client.clientV3.request).toHaveBeenCalledWith({ method: 'delete', url: '/comment/10000' }); expect(result).toBe(true); }); }); @@ -206,9 +213,9 @@ describe('JiraClient', () => { test('should handle axios errors', async () => { // Test that errors are properly propagated const error = new Error('Network error'); - client.client.get = jest.fn().mockRejectedValue(error); + client.clientV3.request = jest.fn().mockRejectedValue(error); await expect(client.getIssue('TEST-1')).rejects.toThrow('Network error'); }); }); -}); \ No newline at end of file +});