diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e09d57b5..da436b55 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,5 +85,13 @@ jobs: # CLI E2E tests drive the built `dist/bin/stash.js` through a real # pseudo-terminal via node-pty. Run via turbo so the `^build` + `build` # deps declared on the `test:e2e` task are honored. + # + # `CS_CLIENT_ACCESS_KEY` + `CS_WORKSPACE_CRN` enable the + # `init-with-access-key` e2e test to authenticate against real CTS via + # `AccessKeyStrategy`. The test is `describe.skipIf`-guarded, so this + # is the env it watches for; without it the test simply skips. - name: Run CLI E2E tests + env: + CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} + CS_WORKSPACE_CRN: ${{ secrets.CS_WORKSPACE_CRN }} run: pnpm exec turbo run test:e2e --filter @cipherstash/cli diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md index 0a358728..8bcc51a2 100644 --- a/packages/cli/AGENTS.md +++ b/packages/cli/AGENTS.md @@ -78,7 +78,44 @@ exercise the same code paths. the CLI, gate construction on an env var (the wizard's `getClient()` pattern) so E2E tests can no-op it. +## Auth-flow testing layers + +`@cipherstash/auth@0.36.0` is a NAPI (Rust) module — its HTTP calls +happen below Node's fetch and there is no profile-dir or base-URL +override. The OAuth device-code flow requires a human at the issuer's +web page. Three constraints fall out: + +1. We can't intercept the auth library's HTTP traffic. +2. We can't E2E the device-code path. +3. `vi.mock` does not cross the pty spawn boundary. + +So auth coverage is layered: + +- **Layer A — `vi.mock` unit tests of the orchestration above the NAPI + boundary.** Real CLI code, stubbed library. Lives in + `src/commands/auth/__tests__/login.test.ts`, + `src/commands/init/steps/__tests__/authenticate.test.ts`, and + `src/auth/__tests__/strategy.test.ts`. Use `tests/helpers/auth-mock.ts` + for `TokenResult` / `DeviceCodeResult` / `AuthError` fixtures. + +- **Layer B — `AccessKeyStrategy` E2E in CI.** A + `describe.skipIf(!CS_CLIENT_ACCESS_KEY)`-guarded test in + `tests/e2e/init-with-access-key.e2e.test.ts` runs `init` against real + CTS via the access-key strategy. CI exposes the secret on the `Run + CLI E2E tests` step (see `.github/workflows/tests.yml`). Locally + without the secret it skips. Doesn't cover the OAuth orchestration — + that's Layer A's job — but it does exercise the full pipe (pty → + CLI → real Rust auth lib → real CTS). + +- **Out of scope.** No fake OAuth server, no + `~/.cipherstash/auth.json` fixturing, no Layer 3 contract test. + Background in `docs/plans/cli-pty-integration-tests.md`. + +When you add a new command that requires authentication, mock +`src/auth/strategy.ts` (the seam) rather than `@cipherstash/auth` +directly — narrower surface, simpler mock setup. + ## Plan and rationale -Background, alternative approaches considered, and the phase-2 messages +Background, alternative approaches considered, and the messages module are in `docs/plans/cli-pty-integration-tests.md`. diff --git a/packages/cli/src/auth/__tests__/strategy.test.ts b/packages/cli/src/auth/__tests__/strategy.test.ts new file mode 100644 index 00000000..8ab53e2a --- /dev/null +++ b/packages/cli/src/auth/__tests__/strategy.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { makeTokenResult } from '../../../tests/helpers/auth-mock.js' + +// Mock only `@cipherstash/auth` here — we want the real `resolveExistingAuth` +// under test, with a stub for the NAPI strategy it constructs. +const napi = vi.hoisted(() => ({ + getToken: vi.fn(), +})) +vi.mock('@cipherstash/auth', () => ({ + default: { + AutoStrategy: { detect: () => ({ getToken: napi.getToken }) }, + AccessKeyStrategy: { create: vi.fn() }, + OAuthStrategy: { fromProfile: vi.fn() }, + beginDeviceCodeFlow: vi.fn(), + bindClientDevice: vi.fn(), + }, +})) + +const { resolveExistingAuth } = await import('../strategy.js') + +describe('resolveExistingAuth', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('maps a known issuer to the matching region label', async () => { + napi.getToken.mockResolvedValueOnce(makeTokenResult()) + + const result = await resolveExistingAuth() + + expect(result).toEqual({ + workspace: 'WS_TEST', + regionLabel: 'ap-southeast-2 (Sydney, Australia)', + }) + }) + + it('returns regionLabel="unknown" when the issuer matches no region', async () => { + napi.getToken.mockResolvedValueOnce( + makeTokenResult({ issuer: 'https://nowhere.example.com' }), + ) + + const result = await resolveExistingAuth() + + expect(result).toEqual({ + workspace: 'WS_TEST', + regionLabel: 'unknown', + }) + }) + + // The catch in resolveExistingAuth is a deliberate "treat any auth error as + // not authenticated" — this is a regression net for the day someone + // narrows it. The codes come from `@cipherstash/auth/index.d.ts:7-22`. + it.each([ + 'NOT_AUTHENTICATED', + 'EXPIRED_TOKEN', + 'INVALID_ACCESS_KEY', + 'MISSING_WORKSPACE_CRN', + 'REQUEST_ERROR', + ])('returns undefined when getToken rejects with %s', async (code) => { + napi.getToken.mockRejectedValueOnce( + Object.assign(new Error(code), { code }), + ) + + const result = await resolveExistingAuth() + + expect(result).toBeUndefined() + }) +}) diff --git a/packages/cli/src/auth/strategy.ts b/packages/cli/src/auth/strategy.ts new file mode 100644 index 00000000..d51ab0c7 --- /dev/null +++ b/packages/cli/src/auth/strategy.ts @@ -0,0 +1,44 @@ +/** + * Thin wrapper around `@cipherstash/auth` strategy detection. + * + * Centralises the NAPI-default-export shape and the swallow-on-failure + * pattern used to decide whether the user is already authenticated. Other + * commands that need an "are we logged in?" check can reuse + * `resolveExistingAuth` instead of duplicating the try/catch + region-label + * lookup. Tests can mock this single module rather than the NAPI library. + */ + +import auth from '@cipherstash/auth' +import { regions } from '../commands/auth/login.js' + +const { AutoStrategy } = auth + +export interface ExistingAuth { + workspace: string + regionLabel: string +} + +/** Construct a fresh `AutoStrategy` — exposed for tests that need to assert on detection. */ +export function getAuthStrategy() { + return AutoStrategy.detect() +} + +/** + * Resolve the currently-authenticated workspace if a valid token is + * available, or `undefined` if not. Errors from `getToken()` (no creds, + * expired tokens, network issues) are swallowed and treated as "not + * authenticated" — the caller decides what to do next (typically: prompt + * the user to log in). + */ +export async function resolveExistingAuth(): Promise { + try { + const result = await getAuthStrategy().getToken() + const regionEntry = regions.find((r) => result.issuer.includes(r.value)) + return { + workspace: result.workspaceId, + regionLabel: regionEntry?.label ?? 'unknown', + } + } catch { + return undefined + } +} diff --git a/packages/cli/src/commands/auth/__tests__/login.test.ts b/packages/cli/src/commands/auth/__tests__/login.test.ts new file mode 100644 index 00000000..2be19119 --- /dev/null +++ b/packages/cli/src/commands/auth/__tests__/login.test.ts @@ -0,0 +1,171 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + makeAuthError, + makeDeviceCodeResult, +} from '../../../../tests/helpers/auth-mock.js' +import { messages } from '../../../messages.js' + +// Hoisted stubs — `vi.mock` factories run before module imports, but we need +// the same fn instances inside the factory and the test bodies so tests can +// reconfigure behaviour per-case via mockResolvedValueOnce / mockRejectedValueOnce. +const stubs = vi.hoisted(() => ({ + beginDeviceCodeFlow: vi.fn(), + bindClientDevice: vi.fn(), +})) + +vi.mock('@cipherstash/auth', () => ({ + default: { + beginDeviceCodeFlow: stubs.beginDeviceCodeFlow, + bindClientDevice: stubs.bindClientDevice, + // The login module destructures only the two functions above, but we + // include the strategy classes so any side-importer doesn't blow up. + AutoStrategy: { detect: vi.fn() }, + AccessKeyStrategy: { create: vi.fn() }, + OAuthStrategy: { fromProfile: vi.fn() }, + }, +})) + +const clack = vi.hoisted(() => ({ + select: vi.fn(), + isCancel: vi.fn(), + cancel: vi.fn(), + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() }, + spinnerStart: vi.fn(), + spinnerStop: vi.fn(), +})) + +vi.mock('@clack/prompts', () => ({ + select: clack.select, + isCancel: clack.isCancel, + cancel: clack.cancel, + log: clack.log, + spinner: () => ({ start: clack.spinnerStart, stop: clack.spinnerStop }), +})) + +// Import after mocks are registered. +const { bindDevice, login, selectRegion } = await import('../login.js') + +describe('selectRegion', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns the chosen region when not cancelled', async () => { + clack.select.mockResolvedValueOnce('us-east-1.aws') + clack.isCancel.mockReturnValueOnce(false) + + const result = await selectRegion() + + expect(result).toBe('us-east-1.aws') + expect(clack.cancel).not.toHaveBeenCalled() + // Sanity: select was called with the message handle, not a literal. + expect(clack.select).toHaveBeenCalledWith( + expect.objectContaining({ message: messages.auth.selectRegion }), + ) + }) + + it('cancels via clack and exits 0 when the prompt is cancelled', async () => { + const exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation((() => undefined) as never) + const cancelSym = Symbol('clack:cancel') + clack.select.mockResolvedValueOnce(cancelSym) + clack.isCancel.mockReturnValueOnce(true) + + await selectRegion() + + expect(clack.cancel).toHaveBeenCalledWith(messages.auth.cancelled) + expect(exitSpy).toHaveBeenCalledWith(0) + exitSpy.mockRestore() + }) +}) + +describe('login', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('runs the full device-code sequence on the happy path', async () => { + const dcr = makeDeviceCodeResult() + stubs.beginDeviceCodeFlow.mockResolvedValueOnce(dcr) + + await login('us-east-1.aws', 'drizzle') + + expect(stubs.beginDeviceCodeFlow).toHaveBeenCalledWith( + 'us-east-1.aws', + // Hardcoded 'cli' OAuth client id — anything else is INVALID_CLIENT. + 'cli', + ) + expect(dcr.openInBrowser).toHaveBeenCalledTimes(1) + expect(clack.spinnerStart).toHaveBeenCalledTimes(1) + expect(dcr.pollForToken).toHaveBeenCalledTimes(1) + expect(clack.spinnerStop).toHaveBeenCalledWith('Authenticated!') + }) + + it('warns when the browser cannot be opened', async () => { + const dcr = makeDeviceCodeResult({ openInBrowser: vi.fn(() => false) }) + stubs.beginDeviceCodeFlow.mockResolvedValueOnce(dcr) + + await login('eu-west-1.aws', undefined) + + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining('Could not open browser'), + ) + }) + + it.each(['EXPIRED_TOKEN', 'ACCESS_DENIED', 'REQUEST_ERROR', 'SERVER_ERROR'])( + 'propagates AuthError(%s) from pollForToken without stopping the spinner', + async (code) => { + const dcr = makeDeviceCodeResult({ + // biome-ignore lint/suspicious/noExplicitAny: cast keeps the narrow AuthErrorCode union accessible to it.each + pollForToken: vi.fn().mockRejectedValueOnce(makeAuthError(code as any)), + }) + stubs.beginDeviceCodeFlow.mockResolvedValueOnce(dcr) + + await expect(login('us-east-1.aws', undefined)).rejects.toMatchObject({ + code, + }) + expect(clack.spinnerStart).toHaveBeenCalledTimes(1) + expect(clack.spinnerStop).not.toHaveBeenCalled() + }, + ) +}) + +describe('bindDevice', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('stops the spinner with success when bindClientDevice resolves', async () => { + stubs.bindClientDevice.mockResolvedValueOnce(undefined) + + await bindDevice() + + expect(stubs.bindClientDevice).toHaveBeenCalledTimes(1) + expect(clack.spinnerStop).toHaveBeenCalledWith( + expect.stringContaining('bound'), + ) + expect(clack.log.error).not.toHaveBeenCalled() + }) + + it('logs the error and exits 1 on bindClientDevice failure', async () => { + const exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation((() => undefined) as never) + stubs.bindClientDevice.mockRejectedValueOnce( + makeAuthError('ACCESS_DENIED', 'no permission'), + ) + + await bindDevice() + + expect(clack.spinnerStop).toHaveBeenCalledWith( + expect.stringContaining('Failed to bind'), + ) + expect(clack.log.error).toHaveBeenCalledWith('no permission') + expect(exitSpy).toHaveBeenCalledWith(1) + }) +}) diff --git a/packages/cli/src/commands/init/steps/__tests__/authenticate.test.ts b/packages/cli/src/commands/init/steps/__tests__/authenticate.test.ts new file mode 100644 index 00000000..ccfb54c3 --- /dev/null +++ b/packages/cli/src/commands/init/steps/__tests__/authenticate.test.ts @@ -0,0 +1,107 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { messages } from '../../../../messages.js' +import type { InitProvider, InitState } from '../../types.js' + +// Mock the auth strategy seam (extracted in src/auth/strategy.ts) so the +// authenticate step has a single, well-typed boundary to mock instead of the +// NAPI default-export from `@cipherstash/auth`. Strategy.ts itself is +// covered by `src/auth/__tests__/strategy.test.ts`. +const strategy = vi.hoisted(() => ({ + resolveExistingAuth: vi.fn(), +})) +vi.mock('../../../../auth/strategy.js', () => ({ + resolveExistingAuth: strategy.resolveExistingAuth, + getAuthStrategy: vi.fn(), +})) + +const innerAuth = vi.hoisted(() => ({ + selectRegion: vi.fn(), + login: vi.fn(), + bindDevice: vi.fn(), +})) +vi.mock('../../../auth/login.js', () => ({ + selectRegion: innerAuth.selectRegion, + login: innerAuth.login, + bindDevice: innerAuth.bindDevice, + regions: [ + { value: 'us-east-1.aws', label: 'us-east-1 (Virginia, USA)' }, + { + value: 'ap-southeast-2.aws', + label: 'ap-southeast-2 (Sydney, Australia)', + }, + ], +})) + +const clack = vi.hoisted(() => ({ + log: { success: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})) +vi.mock('@clack/prompts', () => ({ + log: clack.log, +})) + +const { authenticateStep } = await import('../authenticate.js') + +const provider: InitProvider = { + name: 'drizzle', + introMessage: 'irrelevant', + getNextSteps: () => [], +} + +describe('authenticateStep', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('logs the workspace and skips the device-code flow when already authenticated', async () => { + strategy.resolveExistingAuth.mockResolvedValueOnce({ + workspace: 'WS_TEST', + regionLabel: 'ap-southeast-2 (Sydney, Australia)', + }) + + const state: InitState = {} + const next = await authenticateStep.run(state, provider) + + expect(clack.log.success).toHaveBeenCalledWith( + `${messages.auth.usingWorkspace}WS_TEST (ap-southeast-2 (Sydney, Australia))`, + ) + expect(innerAuth.selectRegion).not.toHaveBeenCalled() + expect(innerAuth.login).not.toHaveBeenCalled() + expect(innerAuth.bindDevice).not.toHaveBeenCalled() + expect(next).toEqual({ authenticated: true }) + }) + + it('falls through to selectRegion → login → bindDevice when not authenticated', async () => { + strategy.resolveExistingAuth.mockResolvedValueOnce(undefined) + innerAuth.selectRegion.mockResolvedValueOnce('ap-southeast-2.aws') + innerAuth.login.mockResolvedValueOnce(undefined) + innerAuth.bindDevice.mockResolvedValueOnce(undefined) + + const next = await authenticateStep.run({}, provider) + + expect(innerAuth.selectRegion).toHaveBeenCalledTimes(1) + expect(innerAuth.login).toHaveBeenCalledWith( + 'ap-southeast-2.aws', + 'drizzle', + ) + expect(innerAuth.bindDevice).toHaveBeenCalledTimes(1) + expect(next).toEqual({ authenticated: true }) + expect(clack.log.success).not.toHaveBeenCalled() + }) + + it('preserves existing state fields on the authenticated path', async () => { + strategy.resolveExistingAuth.mockResolvedValueOnce({ + workspace: 'WS_TEST', + regionLabel: 'unknown', + }) + + const next = await authenticateStep.run( + { clientFilePath: '/tmp/x.ts' }, + provider, + ) + + expect(next).toEqual({ + clientFilePath: '/tmp/x.ts', + authenticated: true, + }) + }) +}) diff --git a/packages/cli/src/commands/init/steps/authenticate.ts b/packages/cli/src/commands/init/steps/authenticate.ts index 1f9d5fb2..73f72813 100644 --- a/packages/cli/src/commands/init/steps/authenticate.ts +++ b/packages/cli/src/commands/init/steps/authenticate.ts @@ -1,38 +1,14 @@ import * as p from '@clack/prompts' -import auth from '@cipherstash/auth' -import { bindDevice, login, regions, selectRegion } from '../../auth/login.js' +import { resolveExistingAuth } from '../../../auth/strategy.js' +import { messages } from '../../../messages.js' +import { bindDevice, login, selectRegion } from '../../auth/login.js' import type { InitProvider, InitState, InitStep } from '../types.js' -const { AutoStrategy } = auth - -interface ExistingAuth { - workspace: string - regionLabel: string -} - -/** - * Check if the user is already authenticated with a valid token. - * Uses OAuthStrategy.getToken() which handles refresh automatically. - */ -async function checkExistingAuth(): Promise { - try { - const strategy = AutoStrategy.detect() - const result = await strategy.getToken() - - const regionEntry = regions.find((r) => result.issuer.includes(r.value)) - const regionLabel = regionEntry?.label ?? 'unknown' - - return { workspace: result.workspaceId, regionLabel } - } catch { - return undefined - } -} - export const authenticateStep: InitStep = { id: 'authenticate', name: 'Authenticate with CipherStash', async run(state: InitState, provider: InitProvider): Promise { - const existing = await checkExistingAuth() + const existing = await resolveExistingAuth() // Already authenticated — silently proceed. Users who want to switch // workspaces can run `stash auth login` directly. Asking on every @@ -40,7 +16,7 @@ export const authenticateStep: InitStep = { // flow. if (existing) { p.log.success( - `Using workspace ${existing.workspace} (${existing.regionLabel})`, + `${messages.auth.usingWorkspace}${existing.workspace} (${existing.regionLabel})`, ) return { ...state, authenticated: true } } diff --git a/packages/cli/src/messages.ts b/packages/cli/src/messages.ts index 75238656..09c914b6 100644 --- a/packages/cli/src/messages.ts +++ b/packages/cli/src/messages.ts @@ -20,6 +20,8 @@ export const messages = { unknownSubcommand: 'Unknown auth command', selectRegion: 'Select a region', cancelled: 'Cancelled.', + /** Prefix of the "Using workspace ()" success log emitted by init's authenticate step. */ + usingWorkspace: 'Using workspace ', }, db: { unknownSubcommand: 'Unknown db subcommand', diff --git a/packages/cli/tests/e2e/init-with-access-key.e2e.test.ts b/packages/cli/tests/e2e/init-with-access-key.e2e.test.ts new file mode 100644 index 00000000..bf254828 --- /dev/null +++ b/packages/cli/tests/e2e/init-with-access-key.e2e.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { messages } from '../../src/messages.js' +import { render } from '../helpers/pty.js' + +const HAS_KEY = + !!process.env.CS_CLIENT_ACCESS_KEY && !!process.env.CS_WORKSPACE_CRN + +// Layer B in `docs/plans/cli-pty-integration-tests.md` (auth follow-up): +// when CI provides `CS_CLIENT_ACCESS_KEY` + `CS_WORKSPACE_CRN`, run `init` +// against real CTS via `AccessKeyStrategy`. Local devs without the secrets +// see this as skipped, never as a flake. +// +// What this *does* exercise: pty → spawned CLI → real `@cipherstash/auth` +// NAPI → real CTS → `authenticateStep` consume-side rendering. +// What it *does not*: the OAuth device-code orchestration in +// `commands/auth/login.ts` — that's covered by the unit suite. +describe.skipIf(!HAS_KEY)('init — real-CTS auth via access key', () => { + it('detects the access key and logs "Using workspace …"', async () => { + // Cast through string — the `skipIf` guard above ensures both vars are + // populated before this body runs, but Biome's noNonNullAssertion + // doesn't follow the guard. + 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) + }) +}) diff --git a/packages/cli/tests/helpers/auth-mock.ts b/packages/cli/tests/helpers/auth-mock.ts new file mode 100644 index 00000000..93a822b4 --- /dev/null +++ b/packages/cli/tests/helpers/auth-mock.ts @@ -0,0 +1,72 @@ +/** + * Test fixtures for `@cipherstash/auth`. + * + * Factories live here so tests don't drift from the real `TokenResult` / + * `AuthError` shapes. The actual `vi.mock(...)` calls have to sit at + * top-level inside each test file (Vitest hoists them) — this module just + * gives you the canned values to feed into those mocks. + */ + +import type { AuthErrorCode, TokenResult } from '@cipherstash/auth' +import { vi } from 'vitest' + +export function makeTokenResult( + overrides: Partial = {}, +): TokenResult { + return { + token: 'test-bearer-token', + subject: 'CS|test-user', + workspaceId: 'WS_TEST', + // Issuer that matches the `ap-southeast-2.aws` region entry in + // commands/auth/login.ts so the `regions.find(...)` lookup resolves. + issuer: 'https://ap-southeast-2.aws.cts.cipherstashmanaged.net', + services: { + zerokms: 'https://ap-southeast-2.aws.zerokms.cipherstashmanaged.net', + }, + ...overrides, + } +} + +/** Build an `AuthError`-shaped error with a documented `.code` from `AuthErrorCode`. */ +export function makeAuthError( + code: AuthErrorCode, + message?: string, +): Error & { code: AuthErrorCode } { + const err = new Error(message ?? `auth error: ${code}`) as Error & { + code: AuthErrorCode + } + err.code = code + return err +} + +/** + * Stand-in for `DeviceCodeResult`. Includes vi-mocked `pollForToken` and + * `openInBrowser` so callers can assert on call sequence and inject + * resolutions/rejections per test. + */ +export function makeDeviceCodeResult( + overrides: { + userCode?: string + verificationUri?: string + verificationUriComplete?: string + expiresIn?: number + pollForToken?: ReturnType + openInBrowser?: ReturnType + } = {}, +) { + return { + userCode: overrides.userCode ?? 'TEST-CODE', + verificationUri: overrides.verificationUri ?? 'https://login.test/activate', + verificationUriComplete: + overrides.verificationUriComplete ?? + 'https://login.test/activate?code=TEST-CODE', + expiresIn: overrides.expiresIn ?? 600, + pollForToken: + overrides.pollForToken ?? + vi.fn().mockResolvedValue({ + expiresAt: Math.floor(Date.now() / 1000) + 3600, + expiresIn: 3600, + }), + openInBrowser: overrides.openInBrowser ?? vi.fn().mockReturnValue(true), + } +} diff --git a/turbo.json b/turbo.json index 6bfd9160..a289a32a 100644 --- a/turbo.json +++ b/turbo.json @@ -21,7 +21,8 @@ "test:e2e": { "dependsOn": ["^build", "build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], - "cache": false + "cache": false, + "passThroughEnv": ["CS_CLIENT_ACCESS_KEY", "CS_WORKSPACE_CRN"] } } }