Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"dev": "tsx src/cli.tsx"
},
"dependencies": {
"conf": "^15.1.0",
"incur": "^0.4.26",
"ink": "^5.2.1",
"ink-spinner": "^5.0.0",
Expand Down
40 changes: 20 additions & 20 deletions packages/cli/src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { execFile } from 'node:child_process';
import http from 'node:http';
import { promisify } from 'node:util';
import { storage } from '@stripe/link-sdk';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { storage } from '../auth/storage';

const execFileAsync = promisify(execFile);

Expand All @@ -27,7 +27,7 @@ function parseJson(raw: string): unknown {

beforeEach(() => {
storage.clearAll();
storage.setAuth(AUTH_TOKENS);
storage.setTokens(AUTH_TOKENS);
});

afterAll(() => {
Expand Down Expand Up @@ -212,7 +212,7 @@ describe('production mode', () => {
responsesByUrl = {};
merchantRequests = [];
merchantResponses = [];
storage.setAuth(PROD_AUTH_TOKENS);
storage.setTokens(PROD_AUTH_TOKENS);
setNextResponse(200, BASE_REQUEST);
});

Expand Down Expand Up @@ -1302,7 +1302,7 @@ describe('production mode', () => {
});

it('rejects unauthenticated requests before hitting the API', async () => {
storage.clearAuth();
storage.clearTokens();

const result = await runProdCli('shipping-address', 'list', '--json');

Expand Down Expand Up @@ -1413,7 +1413,7 @@ describe('production mode', () => {
});

it('rejects unauthenticated requests before hitting the API', async () => {
storage.clearAuth();
storage.clearTokens();

const result = await runProdCli('transactions', 'list', '--json');

Expand Down Expand Up @@ -1494,7 +1494,7 @@ describe('production mode', () => {
});

it('rejects unauthenticated requests before hitting the API', async () => {
storage.clearAuth();
storage.clearTokens();

const result = await runProdCli('sources', 'list', '--json');

Expand Down Expand Up @@ -1574,7 +1574,7 @@ describe('production mode', () => {
});

it('rejects unauthenticated requests before hitting the API', async () => {
storage.clearAuth();
storage.clearTokens();

const result = await runProdCli('balances', 'list', '--json');

Expand Down Expand Up @@ -1670,7 +1670,7 @@ describe('production mode', () => {
});

it('passes a normalized custom --scope to /device/code', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);

const result = await runProdCli(
Expand All @@ -1694,7 +1694,7 @@ describe('production mode', () => {
});

it('does not translate source-related --scope values into authorization_details', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);

const result = await runProdCli(
Expand All @@ -1721,7 +1721,7 @@ describe('production mode', () => {
});

it('passes source actions via authorization_details', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);

const result = await runProdCli(
Expand Down Expand Up @@ -1754,7 +1754,7 @@ describe('production mode', () => {
});

it('passes freeform authorization_details entries after source actions', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);

const result = await runProdCli(
Expand Down Expand Up @@ -1886,7 +1886,7 @@ describe('production mode', () => {
});

it('skips revoke when not previously authenticated', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);

const result = await runProdCli(
Expand All @@ -1905,7 +1905,7 @@ describe('production mode', () => {
});

it('with --interval, yields code first then polls until authenticated', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/revoke', 200, 'ok');
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);
setResponseForUrl('/device/token', 200, TOKEN_RESPONSE);
Expand Down Expand Up @@ -1935,7 +1935,7 @@ describe('production mode', () => {
});

it('with --interval, yields unauthenticated status on timeout (exit 0)', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);
setResponseForUrl('/device/token', 400, {
error: 'authorization_pending',
Expand All @@ -1960,7 +1960,7 @@ describe('production mode', () => {
});

it('with --interval, exits with error on access_denied', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/revoke', 200, 'ok');
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);
setResponseForUrl('/device/token', 400, { error: 'access_denied' });
Expand Down Expand Up @@ -2025,7 +2025,7 @@ describe('production mode', () => {
// Deferred lifecycle: the existing session is preserved (NOT cleared) and
// the pending is flagged so the poll completes the new approval and
// revokes the old grant only once the widened tokens land.
expect(storage.getAuth()).not.toBeNull();
expect(storage.getTokens()).not.toBeNull();
expect(storage.getPendingDeviceAuth()?.replaces_existing_session).toBe(
true,
);
Expand Down Expand Up @@ -2139,7 +2139,7 @@ describe('production mode', () => {
});

it('warns and continues when there is no active session', async () => {
storage.clearAuth();
storage.clearTokens();
setResponseForUrl('/device/code', 200, DEVICE_CODE_RESPONSE);

const result = await runProdCli(
Expand Down Expand Up @@ -2255,7 +2255,7 @@ describe('production mode', () => {
});

it('succeeds when no auth tokens are stored', async () => {
storage.clearAuth();
storage.clearTokens();

const result = await runProdCli('auth', 'logout', '--format', 'json');

Expand All @@ -2271,7 +2271,7 @@ describe('production mode', () => {

describe('auth guard', () => {
it('rejects unauthenticated requests before hitting the API', async () => {
storage.clearAuth();
storage.clearTokens();

const result = await runProdCli(
'spend-request',
Expand Down Expand Up @@ -2300,7 +2300,7 @@ describe('production mode', () => {
const ENV_TOKEN = 'env_access_token_abc123';

beforeEach(() => {
storage.clearAuth();
storage.clearTokens();
});

it('allows user-info retrieve with no stored auth', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/cli/src/auth/__tests__/auth-resource.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { hostname } from 'node:os';
import {
LinkApiError,
LinkAuthorizationDeclinedError,
LinkTransportError,
} from '@stripe/link-sdk';
import { LinkApiError, LinkTransportError } from '@stripe/link-sdk';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LinkAuthResource } from '../auth-resource';
import { LinkAuthorizationDeclinedError } from '../errors';

const mockFetch = vi.fn();

Expand Down
54 changes: 32 additions & 22 deletions packages/cli/src/auth/__tests__/session.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import { LinkAuthenticationError, MemoryStorage } from '@stripe/link-sdk';
import { describe, expect, it, vi } from 'vitest';
import { LinkAuthenticationError } from '../errors';
import { createAccessTokenProvider } from '../session';
import type { IAuthResource } from '../types';
import type { AuthStorage, AuthTokens, IAuthResource } from '../types';

class MemoryAuthStorage implements AuthStorage {
private tokens: AuthTokens | null;

constructor(tokens: AuthTokens | null = null) {
this.tokens = tokens;
}

getTokens(): AuthTokens | null {
return this.tokens;
}

setTokens(tokens: AuthTokens): void {
this.tokens = tokens;
}

clearTokens(): void {
this.tokens = null;
}
}

function createMockAuthRepo(
refreshResult = {
Expand All @@ -21,7 +41,7 @@ function createMockAuthRepo(

describe('createAccessTokenProvider', () => {
it('throws LinkAuthenticationError with not_authenticated code when no auth stored', async () => {
const storage = new MemoryStorage(null);
const storage = new MemoryAuthStorage(null);
const repo = createMockAuthRepo();
const provider = createAccessTokenProvider(repo, storage);

Expand All @@ -34,11 +54,12 @@ describe('createAccessTokenProvider', () => {
});

it('returns cached token when not expired', async () => {
const storage = new MemoryStorage({
const storage = new MemoryAuthStorage({
access_token: 'at_cached',
refresh_token: 'rt_123',
expires_in: 3600,
token_type: 'Bearer',
expires_at: Date.now() + 3_600_000,
});
const repo = createMockAuthRepo();
const provider = createAccessTokenProvider(repo, storage);
Expand All @@ -48,17 +69,10 @@ describe('createAccessTokenProvider', () => {
});

it('refreshes token when expired (within 60s buffer)', async () => {
const storage = new MemoryStorage({
access_token: 'at_old',
refresh_token: 'rt_123',
expires_in: 30, // 30s, will be within 60s buffer after MemoryStorage computes expires_at
token_type: 'Bearer',
});
// Override expires_at to be within the buffer
storage.setAuth({
const storage = new MemoryAuthStorage({
access_token: 'at_old',
refresh_token: 'rt_123',
expires_in: 0,
expires_in: 30,
token_type: 'Bearer',
expires_at: Date.now() + 30_000,
});
Expand All @@ -72,11 +86,12 @@ describe('createAccessTokenProvider', () => {
});

it('refreshes token when forceRefresh is true', async () => {
const storage = new MemoryStorage({
const storage = new MemoryAuthStorage({
access_token: 'at_cached',
refresh_token: 'rt_123',
expires_in: 3600,
token_type: 'Bearer',
expires_at: Date.now() + 3_600_000,
});
const repo = createMockAuthRepo();
const provider = createAccessTokenProvider(repo, storage);
Expand All @@ -88,13 +103,7 @@ describe('createAccessTokenProvider', () => {
});

it('throws when noRefresh is true and token is expired', async () => {
const storage = new MemoryStorage({
access_token: 'at_old',
refresh_token: 'rt_123',
expires_in: 0,
token_type: 'Bearer',
});
storage.setAuth({
const storage = new MemoryAuthStorage({
access_token: 'at_old',
refresh_token: 'rt_123',
expires_in: 0,
Expand All @@ -111,11 +120,12 @@ describe('createAccessTokenProvider', () => {
});

it('throws when noRefresh is true and forceRefresh is requested', async () => {
const storage = new MemoryStorage({
const storage = new MemoryAuthStorage({
access_token: 'at_cached',
refresh_token: 'rt_123',
expires_in: 3600,
token_type: 'Bearer',
expires_at: Date.now() + 3_600_000,
});
const repo = createMockAuthRepo();
const provider = createAccessTokenProvider(repo, storage, {
Expand Down
Loading
Loading