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
66 changes: 66 additions & 0 deletions __tests__/e2e/__snapshots__/stale-auth.e2e.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,49 @@ exports[`when auth token is stale > allows re-authentication after expired token
↑↓ navigate enter confirm vX.Y.Z"
`;

exports[`when auth token is stale > allows signing in after failed refresh and proceeds normally > auth-refresh-failed-then-signed-in 1`] = `
" Confidence by Spotify https://confidence.spotify.com/

Teach your AI Confidence Todo (2/5)

Plugins give your agent tool Confidence-specific skills — flag ● Check system
management, warehouse setup, migrations, onboarding — no more ● Sign in to Confidence
searching docs yourself. ▶ Set up your agent
○ Connect tools
○ Onboard project


──────────────────────────────────────────────────────────────────────────────────────────────────
Which agent tool are you using?

❯ Claude Code
Cursor
Codex
Skip (install manually later)

↑↓ navigate enter confirm vX.Y.Z"
`;

exports[`when auth token is stale > falls back to sign-in when choosing existing account and refresh fails > auth-refresh-failed 1`] = `
" Confidence by Spotify https://confidence.spotify.com/

Sign in to Confidence Todo (1/5)

Sign in so the wizard can create flags and set up your project. ● Check system
▶ Sign in to Confidence
Your session seems to be expired. Please sign in again. ○ Set up your agent
○ Connect tools
○ Onboard project


──────────────────────────────────────────────────────────────────────────────────────────────────
We'll open your browser to sign in. Continue?

❯ Sign in to a Confidence account

enter confirm vX.Y.Z"
`;

exports[`when auth token is stale > prompts user to sign in again instead of using the expired token > auth-expired 1`] = `
" Confidence by Spotify https://confidence.spotify.com/

Expand All @@ -43,6 +86,29 @@ exports[`when auth token is stale > prompts user to sign in again instead of usi
enter confirm vX.Y.Z"
`;

exports[`when auth token is stale > refreshes and authenticates when choosing existing account > auth-refreshed 1`] = `
" Confidence by Spotify https://confidence.spotify.com/

Teach your AI Confidence Todo (2/5)

Plugins give your agent tool Confidence-specific skills — flag ● Check system
management, warehouse setup, migrations, onboarding — no more ● Sign in to Confidence
searching docs yourself. ▶ Set up your agent
○ Connect tools
○ Onboard project


──────────────────────────────────────────────────────────────────────────────────────────────────
Which agent tool are you using?

❯ Claude Code
Cursor
Codex
Skip (install manually later)

↑↓ navigate enter confirm vX.Y.Z"
`;

exports[`when auth token is stale > shows "Use existing account" when the token is still valid > auth-existing 1`] = `
" Confidence by Spotify https://confidence.spotify.com/

Expand Down
7 changes: 7 additions & 0 deletions __tests__/e2e/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export function createSession({
extraArgs = [],
env = {},
token,
refreshToken = 'e2e-refresh-token',
systemPath,
}: {
project?: ProjectType;
extraArgs?: string[];
env?: Record<string, string>;
token?: string;
refreshToken?: string | null;
systemPath?: string;
} = {}): TerminalSession {
const mockBinDir = process.env.E2E_MOCK_BIN_DIR!;
Expand All @@ -47,7 +49,12 @@ export function createSession({

if (token) {
const tokenDir = mkdtempSync(join(tmpdir(), 'e2e-tmp-'));

writeFileSync(join(tokenDir, 'confidence_token'), token, 'utf-8');
if (refreshToken) {
writeFileSync(join(tokenDir, 'confidence_refresh_token'), refreshToken, 'utf-8');
}

sessionEnv.TMPDIR = tokenDir;
}

Expand Down
50 changes: 50 additions & 0 deletions __tests__/e2e/stale-auth.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ describe('when auth token is stale', () => {
expect(session.snapshot()).toMatchSnapshot('auth-existing');
});

it('refreshes and authenticates when choosing existing account', async () => {
using session = createSession({ token: buildTestJwt() });

await navigatePastWelcome(session);

// Authenticate — click "Use existing account", token is refreshed via mock server
await session.waitForText('Use existing account');
await session.sendKey(ENTER);
await session.waitForText('Authenticated');

// Continues to InstallPlugins
await session.waitForText('Which agent tool are you using?');
expect(session.snapshot()).toMatchSnapshot('auth-refreshed');
});

it('falls back to sign-in when choosing existing account and refresh fails', async () => {
using session = createSession({ token: buildTestJwt(), refreshToken: null });

await navigatePastWelcome(session);

// Authenticate — click "Use existing account", refresh fails (no refresh token)
await session.waitForText('Use existing account');
await session.sendKey(ENTER);
await session.waitForText('session seems to be expired');
await session.waitForText('Sign in to a Confidence account');
expect(session.snapshot()).toMatchSnapshot('auth-refresh-failed');
});

it('allows re-authentication after expired token and proceeds normally', async () => {
using session = createSession({ token: buildExpiredJwt() });

Expand All @@ -48,6 +76,28 @@ describe('when auth token is stale', () => {
expect(session.snapshot()).toMatchSnapshot('auth-re-authenticated');
});

it('allows signing in after failed refresh and proceeds normally', async () => {
using session = createSession({ token: buildTestJwt(), refreshToken: null });

await navigatePastWelcome(session);

// Authenticate — click "Use existing account", refresh fails
await session.waitForText('Use existing account');
await session.sendKey(ENTER);
await session.waitForText('session seems to be expired');

// Sign in via browser
await session.waitForText('Sign in to a Confidence account');
await session.sendKey(ENTER);
await session.waitForText('Waiting for browser');
await simulateAuthCallback();
await session.waitForText('Authenticated');

// Continues to InstallPlugins
await session.waitForText('Which agent tool are you using?');
expect(session.snapshot()).toMatchSnapshot('auth-refresh-failed-then-signed-in');
});

it('treats a near-expiry token as valid', async () => {
const nearExpiryJwt = buildTestJwt({ exp: Math.floor(Date.now() / 1000) + 5 });
using session = createSession({ token: nearExpiryJwt });
Expand Down
8 changes: 6 additions & 2 deletions __tests__/msw/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ const ALLOWED_HOSTS: string[] = [];

beforeAll(() =>
server.listen({
onUnhandledRequest(request, print) {
onUnhandledRequest(request) {
const url = new URL(request.url);

if (ALLOWED_HOSTS.includes(url.hostname)) return;
print.warning();

throw new Error(
`[MSW] Unhandled ${request.method} ${url.href}. Add a handler or allowlist the host.`,
);
},
}),
);
Expand Down
29 changes: 29 additions & 0 deletions __tests__/ui/helpers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { writeFileSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import type { AuthState } from '@lib/session.js';

function base64url(str: string): string {
Expand Down Expand Up @@ -28,3 +31,29 @@ export function buildAuthState(token?: string): AuthState {
region: 'EU',
};
}

export function persistTestTokens(token: string, refreshToken?: string) {
const tokenPath = join(tmpdir(), 'confidence_token');
const refreshPath = join(tmpdir(), 'confidence_refresh_token');
const config = { encoding: 'utf-8', mode: 0o600 } as const;

writeFileSync(tokenPath, token, config);
if (refreshToken) {
writeFileSync(refreshPath, refreshToken, config);
}

return {
[Symbol.dispose]() {
try {
unlinkSync(tokenPath);
} catch {
// File may not exist
}
try {
unlinkSync(refreshPath);
} catch {
// File may not exist
}
},
};
}
2 changes: 1 addition & 1 deletion __tests__/ui/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export { delay } from './delay.js';
export { waitFor } from './waitFor.js';
export { createProjectDir } from './project.js';
export { createFakeChild, mockNextSpawn } from './spawn.js';
export { buildTestJwt, buildExpiredJwt, buildAuthState } from './auth.js';
export { buildTestJwt, buildExpiredJwt, buildAuthState, persistTestTokens } from './auth.js';
Loading