Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/platform/cloud/onboarding/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { fromPartial } from '@total-typescript/shoehorn'
import { beforeEach, describe, expect, test, vi } from 'vitest'

import { getSurveyCompletedStatus } from './auth'

const fetchApi = vi.fn()

vi.mock('@/scripts/api', () => ({
api: {
fetchApi: (...args: unknown[]) => fetchApi(...args)
}
}))

vi.mock('@sentry/vue', () => ({
addBreadcrumb: vi.fn(),
captureException: vi.fn()
}))

function mockResponse({
ok,
status,
body
}: {
ok: boolean
status: number
body?: unknown
}): Response {
return fromPartial<Response>({
ok,
status,
statusText: '',
json: async () => body
Comment on lines +19 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: body?: unknown + json: async () => body + as unknown as Response is the kind of double-cast the project's type-safety guidance gently warns against. Acceptable for a test scaffold and I would not block on it, but a typed Partial<Response> helper (or body: { value: unknown } since that's all the SUT reads) would keep the test more honest about the shape it depends on. Non-blocking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction on myself — this codebase has @total-typescript/shoehorn already in dev deps and ~70 test files use it for exactly this. fromPartial<Response>(...) keeps the same partial-shape style without the as unknown as Response double cast (add import { fromPartial } from '@total-typescript/shoehorn' at the top):

Suggested change
function mockResponse({
ok,
status,
body
}: {
ok: boolean
status: number
body?: unknown
}): Response {
return {
ok,
status,
statusText: '',
json: async () => body
function mockResponse({
ok,
status,
body
}: {
ok: boolean
status: number
body?: unknown
}): Response {
return fromPartial<Response>({
ok,
status,
statusText: '',
json: async () => body
})
}

Still non-blocking, just pointing at the local convention. Sorry for the noise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to fromPartial in 1e9a6f5. Thanks for the pointer to the local convention.

})
}

describe('getSurveyCompletedStatus', () => {
beforeEach(() => {
fetchApi.mockReset()
})

test('200 with non-empty value → true', async () => {
fetchApi.mockResolvedValueOnce(
mockResponse({ ok: true, status: 200, body: { value: { q1: 'a' } } })
)
await expect(getSurveyCompletedStatus()).resolves.toBe(true)
})

test('200 with empty value → false (the only "not completed" signal)', async () => {
fetchApi.mockResolvedValueOnce(
mockResponse({ ok: true, status: 200, body: { value: {} } })
)
await expect(getSurveyCompletedStatus()).resolves.toBe(false)
})

test('200 with null value → false', async () => {
fetchApi.mockResolvedValueOnce(
mockResponse({ ok: true, status: 200, body: { value: null } })
)
await expect(getSurveyCompletedStatus()).resolves.toBe(false)
})

test('404 → true (do not bounce on missing key)', async () => {
fetchApi.mockResolvedValueOnce(mockResponse({ ok: false, status: 404 }))
await expect(getSurveyCompletedStatus()).resolves.toBe(true)
})

test('500 → true (do not bounce on transient backend error)', async () => {
fetchApi.mockResolvedValueOnce(mockResponse({ ok: false, status: 500 }))
await expect(getSurveyCompletedStatus()).resolves.toBe(true)
})

// 401/403 fall under the same "ambiguous => treat as completed" policy.
// The dedicated auth layer handles re-authentication on the next API
// call; this function deliberately does not try to disambiguate auth
// failures from other non-OK responses. Locking with tests so the
// policy can't drift back to a "throw on auth error" branch.
test('401 → true (auth layer handles re-auth on next call)', async () => {
fetchApi.mockResolvedValueOnce(mockResponse({ ok: false, status: 401 }))
await expect(getSurveyCompletedStatus()).resolves.toBe(true)
})

Comment on lines +72 to +81
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nit-positive: this is the right way to write a regression test. The 401/403 cases carry an explanatory comment that pins the policy decision to a reasonauth layer handles re-auth on the next API call — so a future refactor that re-introduces a throw branch will trip a red test with a comment explaining why the test is red. Great use of test-as-spec.

test('403 → true (auth layer handles re-auth on next call)', async () => {
fetchApi.mockResolvedValueOnce(mockResponse({ ok: false, status: 403 }))
await expect(getSurveyCompletedStatus()).resolves.toBe(true)
})

test('network rejection → true (do not bounce on network error)', async () => {
fetchApi.mockRejectedValueOnce(new TypeError('Network request failed'))
await expect(getSurveyCompletedStatus()).resolves.toBe(true)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
13 changes: 7 additions & 6 deletions src/platform/cloud/onboarding/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,24 @@ export async function getSurveyCompletedStatus(): Promise<boolean> {
}
})
if (!response.ok) {
// Not an error case - survey not completed is a valid state
// Ambiguous response (404/5xx/etc). Treat as completed to avoid
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nit-positive: this comment is doing what the AGENTS.md guidance asks for — it explains the user-facing why (transient hiccups bouncing working customers), not the what. Future reader will know why this looks like a swallow and won't "helpfully" revert it. Keep this style.

// bouncing working customers to /cloud/survey on transient hiccups.
// Real "not completed" only comes from a 200 with empty value.
Sentry.addBreadcrumb({
category: 'auth',
message: 'Survey status check returned non-ok response',
level: 'info',
level: 'warning',
data: {
status: response.status,
endpoint: `/settings/${ONBOARDING_SURVEY_KEY}`
}
})
return false
return true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ PR description is now stale — please update before merge.

The PR body's resolution table still says:

| 401 / 403 | false → bounced | throws (auth issue, not survey signal) |

…but the final commit (9dfb501c test(auth): lock 401/403 behavior under the ambiguous-response policy) and the new tests at L71–80 explicitly lock 401/403 → true (not throws). Every non-OK response now flows through this single return true; the function has no auth-error branch at all.

This is the only thing I would call out as actually important — reviewers / approvers / future spelunkers will read the PR body and sign off on a different contract than the one being shipped. Please update the table to show 401/403 → true and drop the "throws" row. (Bonus: the simplification story gets even stronger when the table reflects what actually shipped.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the PR description — dropped the throws row, all non-OK / network now show true. Thanks for catching it.

}
const data = await response.json()
// Check if data exists and is not empty
return !isEmpty(data.value)
} catch (error) {
// Network error - still capture it as it's not thrown from above
// Network/parse failure — same policy as ambiguous HTTP responses.
Sentry.captureException(error, {
tags: {
api_endpoint: '/settings/{key}',
Expand All @@ -124,7 +125,7 @@ export async function getSurveyCompletedStatus(): Promise<boolean> {
},
level: 'warning'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor consistency nit: the non-OK HTTP branch above breadcrumbs at level: 'info', but this catch branch logs at level: 'warning'. Under the new policy both are the same event class — "ambiguous response, deliberately resolved as completed." Picking one (probably 'warning' for both, so the suppression is visible in Sentry filters) would make incident triage easier next time someone digs through the breadcrumbs. Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped the breadcrumb to warning in 1e9a6f5 so both ambiguous-response paths log at the same level.

})
return false
return true
}
}

Expand Down
Loading