Skip to content

test(cli): cover the auth flow + auth-gated init step#372

Open
coderdan wants to merge 2 commits intomainfrom
cli-auth-tests
Open

test(cli): cover the auth flow + auth-gated init step#372
coderdan wants to merge 2 commits intomainfrom
cli-auth-tests

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented Apr 30, 2026

Summary

Two-layer test coverage for the CLI's auth orchestration and the auth-gated init step. Plan: docs/plans/cli-pty-integration-tests.md (and the design memo in /Users/dan/.claude/plans/adaptive-mixing-blum.md).

@cipherstash/auth@0.36.0 is a NAPI (Rust) module — its HTTP calls happen below Node's fetch, there's no profile-dir or base-URL override, and pollForToken() requires a human at the OAuth issuer's web page. Three constraints fix the test boundaries.

Layer A — vi.mock unit tests (no creds; runs everywhere)

Stubs @cipherstash/auth at the JS module boundary. Real CLI code runs above the stub: selectRegion, login, bindDevice, authenticateStep, region-label mapping, error handling.

  • New seam src/auth/strategy.ts (getAuthStrategy, resolveExistingAuth) — replaces the inline checkExistingAuth in init/steps/authenticate.ts. Other auth-gated commands hook in here; tests mock this single file rather than the NAPI default-export.
  • src/commands/auth/__tests__/login.test.ts — 10 tests. selectRegion happy + cancel; login happy + browser-open-fallback + AuthError(EXPIRED_TOKEN / ACCESS_DENIED / REQUEST_ERROR / SERVER_ERROR) propagation; bindDevice happy + failure-with-exit-1.
  • src/commands/init/steps/__tests__/authenticate.test.ts — existing-auth, missing-auth fall-through, state-preservation.
  • src/auth/__tests__/strategy.test.ts — issuer→region mapping, "unknown" fallback, swallow-on-error regression net for each AuthErrorCode.
  • tests/helpers/auth-mock.ts — fixture factories.

Layer B — AccessKeyStrategy E2E (real CTS; CI-only)

tests/e2e/init-with-access-key.e2e.test.tsdescribe.skipIf(!CS_CLIENT_ACCESS_KEY || !CS_WORKSPACE_CRN). Spawns init, waits for the messages.auth.usingWorkspace log line emitted by the consume side, then ctrl-c. Exercises the full pipe — pty → CLI binary → real Rust auth lib → real CTS — but does not cover the OAuth orchestration (Layer A does).

CI workflow: env: block added to the Run CLI E2E tests step exposing CS_CLIENT_ACCESS_KEY + CS_WORKSPACE_CRN (same secrets the existing protect/stack test step already consumes).

Out of scope (deliberate non-goals)

  • E2E of the device-code login path (no machine-replayable seam).
  • Stubbing @cipherstash/auth's HTTP layer (Rust internals).
  • Pre-populating ~/.cipherstash/auth.json (no profile-dir override; refresh tokens have to validate against real CTS).
  • Auth+DB tests for db install/upgrade/push/status/test-connection — separate plan, separate PR.

Test plan

  • pnpm --filter @cipherstash/cli build clean
  • pnpm --filter @cipherstash/cli test — 96 unit tests pass (was 76; +20 from auth tests)
  • pnpm --filter @cipherstash/cli test:e2e — 8 pass + 1 skipped (Layer B, expected without secrets locally)
  • biome check clean on changed files
  • CI runs Layer B against the shared workspace

Summary by CodeRabbit

  • New Features

    • Added support for access key-based authentication during initialization
    • Enhanced existing authentication detection to seamlessly handle multiple authentication methods
  • Documentation

    • Updated documentation on authentication testing approach and scope
  • Tests

    • Expanded authentication test coverage across unit and integration layers
    • Added end-to-end test suite for access key authentication workflows

Layered tests for the OAuth login orchestration and downstream auth
consumption. The library (`@cipherstash/auth`) is a NAPI module with
no JS-level fetch seam, no profile-dir override, and a device-code
flow that requires a human at the OAuth issuer's web page — so two
layers carry the load.

**Layer A — `vi.mock` unit tests (no creds, runs everywhere)**

- Extract `src/auth/strategy.ts` with `getAuthStrategy()` +
  `resolveExistingAuth()`, replacing the inline `checkExistingAuth`
  in `init/steps/authenticate.ts`. Single seam other commands (and
  tests) can hook into.
- `src/commands/auth/__tests__/login.test.ts` (10 tests) — covers
  `selectRegion` happy + cancel, `login` happy + browser-fallback +
  AuthError(EXPIRED_TOKEN/ACCESS_DENIED/REQUEST_ERROR/SERVER_ERROR)
  rejection paths, and `bindDevice` happy + failure (asserts exit 1).
- `src/commands/init/steps/__tests__/authenticate.test.ts` — covers
  existing-auth happy path, missing-auth fall-through to
  selectRegion → login → bindDevice, and state-preservation.
- `src/auth/__tests__/strategy.test.ts` — region-label mapping +
  fallback to "unknown" for unknown issuers + swallow-on-error
  regression net for each `AuthErrorCode` from the library d.ts.
- `tests/helpers/auth-mock.ts` — TokenResult / DeviceCodeResult /
  AuthError fixture factories.

**Layer B — `AccessKeyStrategy` E2E (real CTS, CI-only)**

- `tests/e2e/init-with-access-key.e2e.test.ts` —
  `describe.skipIf(!CS_CLIENT_ACCESS_KEY)`-guarded test that spawns
  `init` and waits for `messages.auth.usingWorkspace`. Skips locally
  without secrets, runs in CI via the new `env:` block on the `Run
  CLI E2E tests` step. Doesn't cover the OAuth orchestration (Layer A
  does) but exercises the full pipe: pty → CLI → real Rust auth lib
  → real CTS → consume-side rendering.
- `messages.auth.usingWorkspace` handle added; `authenticate.ts`
  routes the success log through it.

**Documentation**

- `packages/cli/AGENTS.md` documents the layering, the constraints
  that fix the boundaries, and the rule "mock the strategy seam, not
  the NAPI library, when adding auth-gated commands".

Local: 96 unit tests pass; 8 e2e pass + 1 skipped (Layer B).
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

⚠️ No Changeset found

Latest commit: 2748141

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Warning

Rate limit exceeded

@coderdan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 20 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32c1c19e-0e60-49af-bbca-4eb49c524487

📥 Commits

Reviewing files that changed from the base of the PR and between 352c55b and 2748141.

📒 Files selected for processing (1)
  • turbo.json
📝 Walkthrough

Walkthrough

Authentication infrastructure refactored with new resolveExistingAuth() utility and getAuthStrategy() factory, comprehensive unit test coverage for login/device binding flows, E2E test support for access-key authentication, workflow environment variable setup, and authentication testing strategy documentation.

Changes

Cohort / File(s) Summary
Authentication Strategy Module
packages/cli/src/auth/strategy.ts, packages/cli/src/auth/__tests__/strategy.test.ts
New strategy utility exporting ExistingAuth interface, getAuthStrategy() factory, and resolveExistingAuth() function with mocked behavior verification for token resolution, region mapping, and error handling.
CLI Command Tests
packages/cli/src/commands/auth/__tests__/login.test.ts, packages/cli/src/commands/init/steps/__tests__/authenticate.test.ts
Comprehensive test suites covering device-code flow (region selection, login, device binding with hoisted mocks for auth library and prompts) and authenticate step logic (existing auth detection and fallthrough paths).
E2E Test Infrastructure
packages/cli/tests/e2e/init-with-access-key.e2e.test.ts, packages/cli/tests/helpers/auth-mock.ts
PTY-based E2E test for access-key-gated init flow and test fixture factory functions for consistent auth mocking across unit tests.
Authenticate Step Refactoring
packages/cli/src/commands/init/steps/authenticate.ts
Delegates existing-auth resolution to new resolveExistingAuth() utility and uses centralized messages.auth.usingWorkspace constant for user-facing output.
Documentation & Configuration
.github/workflows/tests.yml, packages/cli/AGENTS.md, packages/cli/src/messages.ts
Workflow adds environment variables for E2E secrets, AGENTS.md documents authentication testing strategy across layers, and messages.ts adds usingWorkspace constant.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as CLI init Command
    participant Auth as Auth Library<br/>(NAPI)
    participant OAuth as OAuth Provider
    participant Prompts as Prompts UI

    User->>CLI: init command
    CLI->>Auth: resolveExistingAuth()
    alt Token exists and valid
        Auth-->>CLI: { workspace, regionLabel }
        CLI->>Prompts: log "Using workspace X"
        CLI-->>User: ✓ authenticated
    else No token or error
        CLI->>Prompts: selectRegion()
        Prompts-->>CLI: region selected
        CLI->>Prompts: beginDeviceCodeFlow()
        Prompts-->>CLI: device code & URIs
        CLI->>Prompts: open browser
        Prompts->>OAuth: user authorizes
        CLI->>Auth: pollForToken()
        OAuth-->>Auth: authorization complete
        Auth-->>CLI: token received
        CLI->>CLI: bindDevice()
        CLI-->>User: ✓ authenticated
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • calvinbrewer
  • auxesis

Poem

🐰 A rabbit hops through auth refactored clean,
With mocks and tokens, testing in between.
Device codes flow, from OAuth to CLI—
E2E tests leap gracefully through the sky! 🌟
Strategy patterns hop with newfound might,
Making authentication testing just right. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'test(cli): cover the auth flow + auth-gated init step' directly and accurately summarizes the main change: adding comprehensive test coverage for CLI authentication flow and the auth-gated init step.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cli-auth-tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 55 minutes and 20 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Turbo strips arbitrary env vars from spawned tasks by default — only its
framework-known list reaches the worker. Without `passThroughEnv`, the
`CS_CLIENT_ACCESS_KEY` / `CS_WORKSPACE_CRN` set on the workflow step
never made it to vitest, so the Layer B `init-with-access-key` test
silently `skipIf`'d on CI.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/cli/tests/e2e/init-with-access-key.e2e.test.ts (1)

22-37: ⚡ Quick win

Ensure PTY cleanup runs even on assertion/timeouts.

This test only tears down the spawned process on the success path. Wrap the body in try/finally so a failed waitFor or assertion doesn’t leak the PTY process into later tests.

Proposed patch
-    const r = render(['init'], {
+    const r = render(['init'], {
       env: {
         CS_CLIENT_ACCESS_KEY: String(process.env.CS_CLIENT_ACCESS_KEY),
         CS_WORKSPACE_CRN: String(process.env.CS_WORKSPACE_CRN),
       },
     })
 
-    // The next step after authenticate will prompt or fail — we don't care
-    // which, only that we got past auth. Token mint can take a few seconds
-    // on a cold CTS instance, so allow a generous window.
-    await r.waitFor(messages.auth.usingWorkspace, 20_000)
-
-    r.key('CtrlC')
-    await r.exit
-    expect(r.output).toContain(messages.auth.usingWorkspace)
+    try {
+      // The next step after authenticate will prompt or fail — we don't care
+      // which, only that we got past auth. Token mint can take a few seconds
+      // on a cold CTS instance, so allow a generous window.
+      await r.waitFor(messages.auth.usingWorkspace, 20_000)
+      expect(r.output).toContain(messages.auth.usingWorkspace)
+    } finally {
+      r.key('CtrlC')
+      await r.exit.catch(() => undefined)
+      r.kill()
+    }
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/tests/e2e/init-with-access-key.e2e.test.ts` around lines 22 -
37, Wrap the test's interactive session lifetime in a try/finally to ensure the
spawned PTY is always torn down: after creating r via render(['init'], ...), put
the waitFor, r.key('CtrlC') and assertion inside a try block and in the finally
block always call and await r.exit (or await r.exit() if it’s a function) to
guarantee cleanup even if waitFor or the assertion times out or throws;
reference the existing variables/functions r, render, waitFor, r.key and r.exit
when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/auth/__tests__/strategy.test.ts`:
- Around line 2-3: Replace ad-hoc error objects in this test with the shared
AuthError fixture from the existing auth mock helper: add an import for
AuthError from '../../../tests/helpers/auth-mock.js' alongside makeTokenResult
and swap the inline error objects used in strategy.test.ts (including the ones
around the current 60-62 region) with the imported AuthError symbol so the tests
use the canonical fixture shape.

---

Nitpick comments:
In `@packages/cli/tests/e2e/init-with-access-key.e2e.test.ts`:
- Around line 22-37: Wrap the test's interactive session lifetime in a
try/finally to ensure the spawned PTY is always torn down: after creating r via
render(['init'], ...), put the waitFor, r.key('CtrlC') and assertion inside a
try block and in the finally block always call and await r.exit (or await
r.exit() if it’s a function) to guarantee cleanup even if waitFor or the
assertion times out or throws; reference the existing variables/functions r,
render, waitFor, r.key and r.exit when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0aa373e-43b0-49c9-9a31-2bcc484d1aa7

📥 Commits

Reviewing files that changed from the base of the PR and between 3d12510 and 352c55b.

📒 Files selected for processing (10)
  • .github/workflows/tests.yml
  • packages/cli/AGENTS.md
  • packages/cli/src/auth/__tests__/strategy.test.ts
  • packages/cli/src/auth/strategy.ts
  • packages/cli/src/commands/auth/__tests__/login.test.ts
  • packages/cli/src/commands/init/steps/__tests__/authenticate.test.ts
  • packages/cli/src/commands/init/steps/authenticate.ts
  • packages/cli/src/messages.ts
  • packages/cli/tests/e2e/init-with-access-key.e2e.test.ts
  • packages/cli/tests/helpers/auth-mock.ts

Comment on lines +2 to +3
import { makeTokenResult } from '../../../tests/helpers/auth-mock.js'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the shared AuthError fixture instead of ad-hoc error objects.

This suite already uses shared fixtures; keep error fixtures consistent too to avoid shape drift in auth tests.

Proposed patch
-import { makeTokenResult } from '../../../tests/helpers/auth-mock.js'
+import { makeAuthError, makeTokenResult } from '../../../tests/helpers/auth-mock.js'
@@
-    napi.getToken.mockRejectedValueOnce(
-      Object.assign(new Error(code), { code }),
-    )
+    napi.getToken.mockRejectedValueOnce(makeAuthError(code))

As per coding guidelines, “When testing authentication flows… Use tests/helpers/auth-mock.ts for TokenResult, DeviceCodeResult, and AuthError fixtures in auth unit tests.”

Also applies to: 60-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/auth/__tests__/strategy.test.ts` around lines 2 - 3, Replace
ad-hoc error objects in this test with the shared AuthError fixture from the
existing auth mock helper: add an import for AuthError from
'../../../tests/helpers/auth-mock.js' alongside makeTokenResult and swap the
inline error objects used in strategy.test.ts (including the ones around the
current 60-62 region) with the imported AuthError symbol so the tests use the
canonical fixture shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant