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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ __tests__/e2e/python/code/apt-archives
__tests__/e2e/apt/code/package-lock.json

.env_test
.env

CLAUDE.md
agent-prompt.md
AGENTS.md
AGENTS.md
.qoder
docs/
257 changes: 257 additions & 0 deletions __tests__/ut/commands/list_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import List from '../../../src/subCommands/list';
import FC from '../../../src/resources/fc';
import { IInputs } from '../../../src/interface';
import { tableShow } from '../../../src/utils';

// Mock dependencies
jest.mock('../../../src/resources/fc');
jest.mock('../../../src/logger', () => ({
_set: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
log: jest.fn(),
}));
jest.mock('../../../src/utils', () => ({
tableShow: jest.fn(),
isAppCenter: jest.fn(),
getUserAgent: jest.fn((userAgent, command) => {
return (
userAgent ||
`Component:fc3;Nodejs:${process.version};OS:${process.platform}-${process.arch};command:${command}`
);
}),
}));

describe('List', () => {
let list: List;
let mockInputs: IInputs;
let mockFcSdk: jest.Mocked<FC>;

const mockFunctionsArray = [
{
functionName: 'test-func-1',
runtime: 'nodejs18',
handler: 'index.handler',
memorySize: 128,
state: 'Active',
lastModifiedTime: '2024-01-01T00:00:00Z',
},
{
functionName: 'test-func-2',
runtime: 'python3.10',
handler: 'main.handler',
memorySize: 256,
state: 'Active',
lastModifiedTime: '2024-01-02T00:00:00Z',
},
];

const mockFunctionsPageBody = {
functions: mockFunctionsArray,
nextToken: 'next-token-123',
};

beforeEach(() => {
mockInputs = {
props: {
region: 'cn-hangzhou',
},
credential: {
AccountID: 'test-account',
AccessKeyID: 'test-access-key-id',
AccessKeySecret: 'test-access-key-secret',
},
args: [],
} as any;

mockFcSdk = new FC(mockInputs.props.region, mockInputs.credential, {}) as jest.Mocked<FC>;
(FC as unknown as jest.Mock).mockImplementation(() => mockFcSdk);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('constructor', () => {
it('should create List instance with valid inputs', () => {
mockInputs.args = [];
list = new List(mockInputs);
expect(list).toBeInstanceOf(List);
});

it('should throw error when region is not specified', () => {
delete mockInputs.props.region;
mockInputs.args = [];
expect(() => new List(mockInputs)).toThrow('Region not specified, please specify --region');
});

it('should use region from command line args over props', () => {
mockInputs.args = ['--region', 'cn-beijing'];
list = new List(mockInputs);
expect(list).toBeInstanceOf(List);
});
});

describe('run - without limit (auto-pagination)', () => {
beforeEach(() => {
mockInputs.args = [];
list = new List(mockInputs);
});

it('should list all functions via auto-pagination when no limit specified', async () => {
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);

const result = await list.run();
expect(mockFcSdk.listFunctions).toHaveBeenCalledWith(undefined);
expect(mockFcSdk.listFunctionsPage).not.toHaveBeenCalled();
expect(result).toEqual({ functions: mockFunctionsArray });
});

it('should list all functions with prefix filter via auto-pagination', async () => {
mockInputs.args = ['--prefix', 'test'];
list = new List(mockInputs);
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);

const result = await list.run();
expect(mockFcSdk.listFunctions).toHaveBeenCalledWith('test');
expect(result).toEqual({ functions: mockFunctionsArray });
});

it('should show table output without limit', async () => {
mockInputs.args = ['--table'];
list = new List(mockInputs);
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);

await list.run();
expect(tableShow).toHaveBeenCalledWith(mockFunctionsArray, [
'functionName',
'runtime',
'handler',
'memorySize',
'state',
'lastModifiedTime',
]);
});

it('should handle empty functions list via auto-pagination', async () => {
mockFcSdk.listFunctions = jest.fn().mockResolvedValue([]);

const result = await list.run();
expect(result).toEqual({ functions: [] });
});

it('should not call tableShow when --table is not specified', async () => {
mockFcSdk.listFunctions = jest.fn().mockResolvedValue(mockFunctionsArray);

await list.run();
expect(tableShow).not.toHaveBeenCalled();
});
});

describe('run - with limit (single page)', () => {
it('should list functions with custom limit via single page', async () => {
mockInputs.args = ['--limit', '20'];
list = new List(mockInputs);
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);

const result = await list.run();
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(20, undefined, undefined);
expect(mockFcSdk.listFunctions).not.toHaveBeenCalled();
expect(result).toEqual(mockFunctionsPageBody);
});

it('should reject --limit with value 0', async () => {
mockInputs.args = ['--limit', '0'];
list = new List(mockInputs);

await expect(list.run()).rejects.toThrow('--limit must be a positive integer');
});

it('should reject --limit with non-integer value', async () => {
mockInputs.args = ['--limit', 'abc'];
list = new List(mockInputs);

await expect(list.run()).rejects.toThrow('--limit must be a positive integer');
});

it('should list functions with prefix and limit', async () => {
mockInputs.args = ['--prefix', 'test', '--limit', '20'];
list = new List(mockInputs);
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);

const result = await list.run();
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(20, 'test', undefined);
expect(result).toEqual(mockFunctionsPageBody);
});

it('should list functions with nextToken for pagination', async () => {
mockInputs.args = ['--limit', '20', '--next-token', 'next-token-123'];
list = new List(mockInputs);
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);

const result = await list.run();
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(20, undefined, 'next-token-123');
expect(result).toEqual(mockFunctionsPageBody);
});

it('should list functions with all query parameters', async () => {
mockInputs.args = ['--prefix', 'test', '--limit', '10', '--next-token', 'token-abc'];
list = new List(mockInputs);
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);

const result = await list.run();
expect(mockFcSdk.listFunctionsPage).toHaveBeenCalledWith(10, 'test', 'token-abc');
expect(result).toEqual(mockFunctionsPageBody);
});

it('should show table output with limit', async () => {
mockInputs.args = ['--limit', '20', '--table'];
list = new List(mockInputs);
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue(mockFunctionsPageBody);

await list.run();
expect(tableShow).toHaveBeenCalledWith(mockFunctionsArray, [
'functionName',
'runtime',
'handler',
'memorySize',
'state',
'lastModifiedTime',
]);
});

it('should handle table output with empty functions via single page', async () => {
mockInputs.args = ['--limit', '20', '--table'];
list = new List(mockInputs);
mockFcSdk.listFunctionsPage = jest.fn().mockResolvedValue({
functions: [],
});

await list.run();
expect(tableShow).toHaveBeenCalledWith(
[],
['functionName', 'runtime', 'handler', 'memorySize', 'state', 'lastModifiedTime'],
);
});
});

describe('run - error handling', () => {
it('should propagate auto-pagination API errors', async () => {
mockInputs.args = [];
list = new List(mockInputs);
mockFcSdk.listFunctions = jest.fn().mockRejectedValue(new Error('API error'));

await expect(list.run()).rejects.toThrow('API error');
});

it('should propagate single page API errors', async () => {
mockInputs.args = ['--limit', '20'];
list = new List(mockInputs);
mockFcSdk.listFunctionsPage = jest.fn().mockRejectedValue(new Error('API error'));

await expect(list.run()).rejects.toThrow('API error');
});
});
});
2 changes: 1 addition & 1 deletion publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Type: Component
Name: fc3
Provider:
- 阿里云
Version: 0.1.18
Version: 0.1.19
Description: 阿里云函数计算全生命周期管理
HomePage: https://github.com/devsapp/fc3
Organization: 阿里云函数计算(FC)
Expand Down
2 changes: 2 additions & 0 deletions src/commands-help/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import s2tos3 from './s2tos3';
import logs from './logs';
import session from './session';
import scaling from './scaling';
import list from './list';

export default {
deploy,
Expand All @@ -36,4 +37,5 @@ export default {
logs,
session,
scaling,
list,
};
26 changes: 26 additions & 0 deletions src/commands-help/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default {
help: {
description: `List all functions.
Example:
$ s list
$ s list --prefix test --table
$ s cli fc3 list --prefix test --limit 20 --next-token xxx --region cn-hangzhou -a default`,
summary: 'List all functions',
option: [
[
'--region <region>',
'[C-Required] Specify the fc region, you can see all supported regions in https://help.aliyun.com/document_detail/2512917.html',
],
['--prefix <prefix>', '[Optional] Specify the prefix of function name'],
[
'--limit <limit>',
'[Optional] Specify the max number of functions to return per page, if not specified, all functions will be listed',
],
[
'--next-token <nextToken>',
'[Optional] Specify the next token for pagination, only works with --limit',
],
['--table', '[Optional] Specify if output the result as table format'],
],
},
};
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Alias from './subCommands/alias';
import Concurrency from './subCommands/concurrency';
import SYaml2To3 from './subCommands/2to3';
import Logs from './subCommands/logs';
import List from './subCommands/list';
import { SCHEMA_FILE_PATH } from './constant';
import { checkDockerIsOK, isAppCenter, isYunXiao } from './utils';
import { Model } from './subCommands/model';
Expand Down Expand Up @@ -183,6 +184,12 @@ export default class Fc extends Base {
return await logs.run();
}

public async list(inputs: IInputs) {
await super.handlePreRun(inputs, true);
const list = new List(inputs);
return await list.run();
}

public async model(inputs: IInputs) {
await super.handlePreRun(inputs, false);
const model = new Model(inputs);
Expand Down
11 changes: 10 additions & 1 deletion src/resources/fc/impl/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,12 +418,21 @@ export default class FC_Client {
return body;
}

async listFunctionsPage(limit: number, prefix?: string, nextToken?: string) {
const request = new ListFunctionsRequest({ limit, prefix, nextToken });
const runtime = new RuntimeOptions({});
const headers: { [key: string]: string } = {};
const result = await this.fc20230330Client.listFunctionsWithOptions(request, headers, runtime);
const { body } = result.toMap();
return body;
}

/**
* list 接口实现模版
*/
async listFunctions(prefix?: string): Promise<any[]> {
let nextToken = '';
const limit = 2;
const limit = 100;
const functions: any[] = [];

while (true) {
Expand Down
Loading
Loading