Skip to content

Commit c6ed6db

Browse files
designcodeclaude
andauthored
test: expand unit and integration test coverage (#85)
* fix: remove compression default so auto-detection from extension works The specs.yaml default: none caused Commander to always set compressionArg, preventing auto-detection from output file extension (.tar.gz → gzip). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: expand unit and integration test coverage Unit tests for previously untested utilities and auth modules: - upload.ts: calculateUploadParams boundary cases - concurrency.ts: pool limiting, ordering, error propagation - bucket-info.ts: all conditional branches (TTL, lifecycle, CORS, etc.) - interactive.ts: TTY detection guard - auth/fly.ts: Fly org prefix detection - auth/iam.ts: OAuth and credential config resolution branches Integration tests for access-keys lifecycle: - create, get, list, assign (bucket + admin), revoke, rotate, delete - Error cases for missing args and confirmation prompts Also adds OAuth-guarded test block for IAM policies, users, and orgs (skipped unless TIGRIS_OAUTH_TEST=true), and whoami command tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 484a2b6 commit c6ed6db

9 files changed

Lines changed: 992 additions & 3 deletions

File tree

src/specs.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,8 @@ commands:
573573
description: Output file path. Defaults to stdout (for piping)
574574
alias: o
575575
- name: compression
576-
description: Compression algorithm for the archive
576+
description: Compression algorithm for the archive. Auto-detected from output file extension when not specified
577577
options: [none, gzip, zstd]
578-
default: none
579578
- name: on-error
580579
description: How to handle missing objects. 'skip' omits them, 'fail' aborts the request
581580
options: [skip, fail]

test/auth/fly.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
vi.mock('../../src/auth/storage.js', () => ({
4+
getSelectedOrganization: vi.fn(),
5+
}));
6+
7+
import { isFlyOrganization } from '../../src/auth/fly.js';
8+
import { getSelectedOrganization } from '../../src/auth/storage.js';
9+
10+
describe('isFlyOrganization', () => {
11+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
12+
13+
beforeEach(() => {
14+
logSpy.mockClear();
15+
});
16+
17+
afterEach(() => {
18+
vi.mocked(getSelectedOrganization).mockReset();
19+
});
20+
21+
it('returns true when org starts with flyio_', () => {
22+
vi.mocked(getSelectedOrganization).mockReturnValue('flyio_my-org');
23+
expect(isFlyOrganization('User management')).toBe(true);
24+
});
25+
26+
it('prints message when org is Fly', () => {
27+
vi.mocked(getSelectedOrganization).mockReturnValue('flyio_my-org');
28+
isFlyOrganization('User management');
29+
expect(logSpy).toHaveBeenCalledTimes(1);
30+
expect(logSpy.mock.calls[0][0]).toContain('User management');
31+
expect(logSpy.mock.calls[0][0]).toContain('fly.io');
32+
});
33+
34+
it('returns false when org does not start with flyio_', () => {
35+
vi.mocked(getSelectedOrganization).mockReturnValue('my-regular-org');
36+
expect(isFlyOrganization('User management')).toBe(false);
37+
expect(logSpy).not.toHaveBeenCalled();
38+
});
39+
40+
it('returns false when getSelectedOrganization returns null', () => {
41+
vi.mocked(getSelectedOrganization).mockReturnValue(null);
42+
expect(isFlyOrganization('User management')).toBe(false);
43+
expect(logSpy).not.toHaveBeenCalled();
44+
});
45+
});

test/auth/iam.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
// Mock dependencies before importing module under test
4+
vi.mock('../../src/auth/client.js', () => ({
5+
getAuthClient: vi.fn(() => ({
6+
isAuthenticated: vi.fn(),
7+
getAccessToken: vi.fn(),
8+
})),
9+
getAuth0Config: () => ({
10+
domain: 'test.auth0.com',
11+
clientId: 'test-client-id',
12+
audience: 'test-audience',
13+
}),
14+
}));
15+
16+
vi.mock('../../src/auth/provider.js', () => ({
17+
resolveAuthMethod: vi.fn(),
18+
getTigrisConfig: vi.fn(() => ({
19+
iamEndpoint: 'https://iam.test',
20+
mgmtEndpoint: 'https://mgmt.test',
21+
})),
22+
}));
23+
24+
vi.mock('../../src/auth/storage.js', () => ({
25+
getLoginMethod: vi.fn(),
26+
getSelectedOrganization: vi.fn(),
27+
}));
28+
29+
vi.mock('../../src/utils/exit.js', () => ({
30+
failWithError: vi.fn((_ctx: unknown, msg: unknown) => {
31+
throw new Error(String(msg));
32+
}),
33+
}));
34+
35+
vi.mock('../../src/utils/messages.js', () => ({
36+
msg: vi.fn(() => ({})),
37+
}));
38+
39+
import { getAuthClient } from '../../src/auth/client.js';
40+
import { getIAMConfig, getOAuthIAMConfig } from '../../src/auth/iam.js';
41+
import { resolveAuthMethod } from '../../src/auth/provider.js';
42+
import {
43+
getLoginMethod,
44+
getSelectedOrganization,
45+
} from '../../src/auth/storage.js';
46+
import { msg } from '../../src/utils/messages.js';
47+
48+
const context = msg('test');
49+
50+
describe('getOAuthIAMConfig', () => {
51+
it('throws when login method is not oauth', async () => {
52+
vi.mocked(getLoginMethod).mockReturnValue('credentials');
53+
await expect(getOAuthIAMConfig(context)).rejects.toThrow(
54+
'requires OAuth login'
55+
);
56+
});
57+
58+
it('throws when not authenticated', async () => {
59+
vi.mocked(getLoginMethod).mockReturnValue('oauth');
60+
const mockClient = {
61+
isAuthenticated: vi.fn().mockResolvedValue(false),
62+
getAccessToken: vi.fn(),
63+
};
64+
vi.mocked(getAuthClient).mockReturnValue(
65+
mockClient as ReturnType<typeof getAuthClient>
66+
);
67+
68+
await expect(getOAuthIAMConfig(context)).rejects.toThrow(
69+
'Not authenticated'
70+
);
71+
});
72+
73+
it('returns config on success', async () => {
74+
vi.mocked(getLoginMethod).mockReturnValue('oauth');
75+
vi.mocked(getSelectedOrganization).mockReturnValue('my-org');
76+
const mockClient = {
77+
isAuthenticated: vi.fn().mockResolvedValue(true),
78+
getAccessToken: vi.fn().mockResolvedValue('tok-123'),
79+
};
80+
vi.mocked(getAuthClient).mockReturnValue(
81+
mockClient as ReturnType<typeof getAuthClient>
82+
);
83+
84+
const config = await getOAuthIAMConfig(context);
85+
expect(config).toEqual({
86+
sessionToken: 'tok-123',
87+
organizationId: 'my-org',
88+
iamEndpoint: 'https://iam.test',
89+
mgmtEndpoint: 'https://mgmt.test',
90+
});
91+
});
92+
93+
it('returns undefined organizationId when no org selected', async () => {
94+
vi.mocked(getLoginMethod).mockReturnValue('oauth');
95+
vi.mocked(getSelectedOrganization).mockReturnValue(null);
96+
const mockClient = {
97+
isAuthenticated: vi.fn().mockResolvedValue(true),
98+
getAccessToken: vi.fn().mockResolvedValue('tok-123'),
99+
};
100+
vi.mocked(getAuthClient).mockReturnValue(
101+
mockClient as ReturnType<typeof getAuthClient>
102+
);
103+
104+
const config = await getOAuthIAMConfig(context);
105+
expect(config.organizationId).toBeUndefined();
106+
});
107+
});
108+
109+
describe('getIAMConfig', () => {
110+
it('delegates to getOAuthIAMConfig when type is oauth', async () => {
111+
vi.mocked(resolveAuthMethod).mockResolvedValue({
112+
type: 'oauth',
113+
} as Awaited<ReturnType<typeof resolveAuthMethod>>);
114+
vi.mocked(getLoginMethod).mockReturnValue('oauth');
115+
const mockClient = {
116+
isAuthenticated: vi.fn().mockResolvedValue(true),
117+
getAccessToken: vi.fn().mockResolvedValue('tok-456'),
118+
};
119+
vi.mocked(getAuthClient).mockReturnValue(
120+
mockClient as ReturnType<typeof getAuthClient>
121+
);
122+
vi.mocked(getSelectedOrganization).mockReturnValue('org-1');
123+
124+
const config = await getIAMConfig(context);
125+
expect(config).toHaveProperty('sessionToken', 'tok-456');
126+
});
127+
128+
it.each(['credentials', 'environment', 'configured', 'aws-profile'] as const)(
129+
'returns credential config when type is %s',
130+
async (type) => {
131+
vi.mocked(resolveAuthMethod).mockResolvedValue({
132+
type,
133+
accessKeyId: 'ak-123',
134+
secretAccessKey: 'sk-456',
135+
} as Awaited<ReturnType<typeof resolveAuthMethod>>);
136+
vi.mocked(getSelectedOrganization).mockReturnValue('org-2');
137+
138+
const config = await getIAMConfig(context);
139+
expect(config).toEqual({
140+
accessKeyId: 'ak-123',
141+
secretAccessKey: 'sk-456',
142+
organizationId: 'org-2',
143+
iamEndpoint: 'https://iam.test',
144+
});
145+
}
146+
);
147+
148+
it('throws when type is none', async () => {
149+
vi.mocked(resolveAuthMethod).mockResolvedValue({
150+
type: 'none',
151+
} as Awaited<ReturnType<typeof resolveAuthMethod>>);
152+
153+
await expect(getIAMConfig(context)).rejects.toThrow('Not authenticated');
154+
});
155+
});

0 commit comments

Comments
 (0)