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
3 changes: 1 addition & 2 deletions src/specs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -573,9 +573,8 @@ commands:
description: Output file path. Defaults to stdout (for piping)
alias: o
- name: compression
description: Compression algorithm for the archive
description: Compression algorithm for the archive. Auto-detected from output file extension when not specified
options: [none, gzip, zstd]
default: none
- name: on-error
description: How to handle missing objects. 'skip' omits them, 'fail' aborts the request
options: [skip, fail]
Expand Down
45 changes: 45 additions & 0 deletions test/auth/fly.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../../src/auth/storage.js', () => ({
getSelectedOrganization: vi.fn(),
}));

import { isFlyOrganization } from '../../src/auth/fly.js';
import { getSelectedOrganization } from '../../src/auth/storage.js';

describe('isFlyOrganization', () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

beforeEach(() => {
logSpy.mockClear();
});

afterEach(() => {
vi.mocked(getSelectedOrganization).mockReset();
});

it('returns true when org starts with flyio_', () => {
vi.mocked(getSelectedOrganization).mockReturnValue('flyio_my-org');
expect(isFlyOrganization('User management')).toBe(true);
});

it('prints message when org is Fly', () => {
vi.mocked(getSelectedOrganization).mockReturnValue('flyio_my-org');
isFlyOrganization('User management');
expect(logSpy).toHaveBeenCalledTimes(1);
expect(logSpy.mock.calls[0][0]).toContain('User management');
expect(logSpy.mock.calls[0][0]).toContain('fly.io');
});

it('returns false when org does not start with flyio_', () => {
vi.mocked(getSelectedOrganization).mockReturnValue('my-regular-org');
expect(isFlyOrganization('User management')).toBe(false);
expect(logSpy).not.toHaveBeenCalled();
});

it('returns false when getSelectedOrganization returns null', () => {
vi.mocked(getSelectedOrganization).mockReturnValue(null);
expect(isFlyOrganization('User management')).toBe(false);
expect(logSpy).not.toHaveBeenCalled();
});
});
155 changes: 155 additions & 0 deletions test/auth/iam.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { describe, expect, it, vi } from 'vitest';

// Mock dependencies before importing module under test
vi.mock('../../src/auth/client.js', () => ({
getAuthClient: vi.fn(() => ({
isAuthenticated: vi.fn(),
getAccessToken: vi.fn(),
})),
getAuth0Config: () => ({
domain: 'test.auth0.com',
clientId: 'test-client-id',
audience: 'test-audience',
}),
}));

vi.mock('../../src/auth/provider.js', () => ({
resolveAuthMethod: vi.fn(),
getTigrisConfig: vi.fn(() => ({
iamEndpoint: 'https://iam.test',
mgmtEndpoint: 'https://mgmt.test',
})),
}));

vi.mock('../../src/auth/storage.js', () => ({
getLoginMethod: vi.fn(),
getSelectedOrganization: vi.fn(),
}));

vi.mock('../../src/utils/exit.js', () => ({
failWithError: vi.fn((_ctx: unknown, msg: unknown) => {
throw new Error(String(msg));
}),
}));

vi.mock('../../src/utils/messages.js', () => ({
msg: vi.fn(() => ({})),
}));

import { getAuthClient } from '../../src/auth/client.js';
import { getIAMConfig, getOAuthIAMConfig } from '../../src/auth/iam.js';
import { resolveAuthMethod } from '../../src/auth/provider.js';
import {
getLoginMethod,
getSelectedOrganization,
} from '../../src/auth/storage.js';
import { msg } from '../../src/utils/messages.js';

const context = msg('test');

describe('getOAuthIAMConfig', () => {
it('throws when login method is not oauth', async () => {
vi.mocked(getLoginMethod).mockReturnValue('credentials');
await expect(getOAuthIAMConfig(context)).rejects.toThrow(
'requires OAuth login'
);
});

it('throws when not authenticated', async () => {
vi.mocked(getLoginMethod).mockReturnValue('oauth');
const mockClient = {
isAuthenticated: vi.fn().mockResolvedValue(false),
getAccessToken: vi.fn(),
};
vi.mocked(getAuthClient).mockReturnValue(
mockClient as ReturnType<typeof getAuthClient>
);

await expect(getOAuthIAMConfig(context)).rejects.toThrow(
'Not authenticated'
);
});

it('returns config on success', async () => {
vi.mocked(getLoginMethod).mockReturnValue('oauth');
vi.mocked(getSelectedOrganization).mockReturnValue('my-org');
const mockClient = {
isAuthenticated: vi.fn().mockResolvedValue(true),
getAccessToken: vi.fn().mockResolvedValue('tok-123'),
};
vi.mocked(getAuthClient).mockReturnValue(
mockClient as ReturnType<typeof getAuthClient>
);

const config = await getOAuthIAMConfig(context);
expect(config).toEqual({
sessionToken: 'tok-123',
organizationId: 'my-org',
iamEndpoint: 'https://iam.test',
mgmtEndpoint: 'https://mgmt.test',
});
});

it('returns undefined organizationId when no org selected', async () => {
vi.mocked(getLoginMethod).mockReturnValue('oauth');
vi.mocked(getSelectedOrganization).mockReturnValue(null);
const mockClient = {
isAuthenticated: vi.fn().mockResolvedValue(true),
getAccessToken: vi.fn().mockResolvedValue('tok-123'),
};
vi.mocked(getAuthClient).mockReturnValue(
mockClient as ReturnType<typeof getAuthClient>
);

const config = await getOAuthIAMConfig(context);
expect(config.organizationId).toBeUndefined();
});
});

describe('getIAMConfig', () => {
it('delegates to getOAuthIAMConfig when type is oauth', async () => {
vi.mocked(resolveAuthMethod).mockResolvedValue({
type: 'oauth',
} as Awaited<ReturnType<typeof resolveAuthMethod>>);
vi.mocked(getLoginMethod).mockReturnValue('oauth');
const mockClient = {
isAuthenticated: vi.fn().mockResolvedValue(true),
getAccessToken: vi.fn().mockResolvedValue('tok-456'),
};
vi.mocked(getAuthClient).mockReturnValue(
mockClient as ReturnType<typeof getAuthClient>
);
vi.mocked(getSelectedOrganization).mockReturnValue('org-1');

const config = await getIAMConfig(context);
expect(config).toHaveProperty('sessionToken', 'tok-456');
});

it.each(['credentials', 'environment', 'configured', 'aws-profile'] as const)(
'returns credential config when type is %s',
async (type) => {
vi.mocked(resolveAuthMethod).mockResolvedValue({
type,
accessKeyId: 'ak-123',
secretAccessKey: 'sk-456',
} as Awaited<ReturnType<typeof resolveAuthMethod>>);
vi.mocked(getSelectedOrganization).mockReturnValue('org-2');

const config = await getIAMConfig(context);
expect(config).toEqual({
accessKeyId: 'ak-123',
secretAccessKey: 'sk-456',
organizationId: 'org-2',
iamEndpoint: 'https://iam.test',
});
}
);

it('throws when type is none', async () => {
vi.mocked(resolveAuthMethod).mockResolvedValue({
type: 'none',
} as Awaited<ReturnType<typeof resolveAuthMethod>>);

await expect(getIAMConfig(context)).rejects.toThrow('Not authenticated');
});
});
Loading