From e3ca72b2b59f3fcf2876cdcc3e77b52308d70f77 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:22:01 -0700 Subject: [PATCH] Fix Jira credentials clearing on forbidden task reads (#5757) Co-authored-by: Orca --- src/main/jira/client.test.ts | 7 +++++++ src/main/jira/client.ts | 4 +++- src/renderer/src/store/slices/jira.test.ts | 12 ++++++++++++ src/renderer/src/store/slices/jira.ts | 4 +++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/jira/client.test.ts b/src/main/jira/client.test.ts index ab949441e..713311b8a 100644 --- a/src/main/jira/client.test.ts +++ b/src/main/jira/client.test.ts @@ -364,6 +364,13 @@ describe('Jira client credential storage', () => { }) }) + it('does not treat Jira permission failures as credential revocation', async () => { + const jira = await loadClientModule() + + expect(jira.isAuthError(new jira.JiraApiError('Unauthorized', 401))).toBe(true) + expect(jira.isAuthError(new jira.JiraApiError('Forbidden', 403))).toBe(false) + }) + it('bridges proxy environment settings before Jira connect requests', async () => { netFetchMock.mockResolvedValueOnce( new Response( diff --git a/src/main/jira/client.ts b/src/main/jira/client.ts index a0e705290..d56e32d35 100644 --- a/src/main/jira/client.ts +++ b/src/main/jira/client.ts @@ -574,5 +574,7 @@ export function clearToken(siteId: string): void { } export function isAuthError(error: unknown): boolean { - return error instanceof JiraApiError && (error.status === 401 || error.status === 403) + // Why: Jira returns 403 for project/API permission gaps even when /myself + // succeeds, so only 401 means the saved credential itself is invalid. + return error instanceof JiraApiError && error.status === 401 } diff --git a/src/renderer/src/store/slices/jira.test.ts b/src/renderer/src/store/slices/jira.test.ts index 565467e05..ebe427f44 100644 --- a/src/renderer/src/store/slices/jira.test.ts +++ b/src/renderer/src/store/slices/jira.test.ts @@ -390,4 +390,16 @@ describe('createJiraSlice credential errors', () => { expect(store.getState().jiraStatus.credentialError).toBeUndefined() }) }) + + it('keeps Jira connected when an issue read hits endpoint-level forbidden access', async () => { + const store = createTestStore() + store.setState({ + jiraStatus: { connected: true, viewer: null, selectedSiteId: 'site-1' } + }) + jiraListIssues.mockRejectedValueOnce(new Error('Forbidden')) + + await expect(store.getState().listJiraIssues('assigned', 30)).resolves.toEqual([]) + + expect(store.getState().jiraStatus.connected).toBe(true) + }) }) diff --git a/src/renderer/src/store/slices/jira.ts b/src/renderer/src/store/slices/jira.ts index 9892ee8e7..1498f312e 100644 --- a/src/renderer/src/store/slices/jira.ts +++ b/src/renderer/src/store/slices/jira.ts @@ -55,7 +55,9 @@ function evictStaleEntries( function looksLikeAuthError(error: unknown): boolean { const msg = error instanceof Error ? error.message : String(error) - return /authenticat|unauthorized|forbidden|401|403/i.test(msg) + // Why: Jira 403 commonly means endpoint/project access is denied while the + // saved token is still valid; do not flip Settings back to disconnected. + return /authenticat|unauthorized|401/i.test(msg) } type InflightJiraReadRequest = {