@capawesome/cli 1.13.1 → 1.14.0-dev.1f912c9.1755635879
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -3
- package/dist/commands/apps/bundles/create.js +215 -233
- package/dist/commands/apps/bundles/create.test.js +274 -0
- package/dist/commands/apps/bundles/delete.js +42 -47
- package/dist/commands/apps/bundles/delete.test.js +140 -0
- package/dist/commands/apps/bundles/update.js +69 -72
- package/dist/commands/apps/bundles/update.test.js +142 -0
- package/dist/commands/apps/channels/create.js +47 -70
- package/dist/commands/apps/channels/create.test.js +115 -0
- package/dist/commands/apps/channels/delete.js +53 -56
- package/dist/commands/apps/channels/delete.test.js +140 -0
- package/dist/commands/apps/channels/get.js +27 -61
- package/dist/commands/apps/channels/get.test.js +136 -0
- package/dist/commands/apps/channels/list.js +26 -63
- package/dist/commands/apps/channels/list.test.js +123 -0
- package/dist/commands/apps/channels/update.js +48 -67
- package/dist/commands/apps/channels/update.test.js +139 -0
- package/dist/commands/apps/create.js +38 -38
- package/dist/commands/apps/create.test.js +117 -0
- package/dist/commands/apps/delete.js +39 -40
- package/dist/commands/apps/delete.test.js +119 -0
- package/dist/commands/apps/devices/delete.js +42 -47
- package/dist/commands/apps/devices/delete.test.js +140 -0
- package/dist/commands/doctor.js +12 -29
- package/dist/commands/doctor.test.js +52 -0
- package/dist/commands/login.js +48 -69
- package/dist/commands/login.test.js +122 -0
- package/dist/commands/logout.js +13 -31
- package/dist/commands/logout.test.js +47 -0
- package/dist/commands/manifests/generate.js +20 -38
- package/dist/commands/manifests/generate.test.js +60 -0
- package/dist/commands/whoami.js +13 -30
- package/dist/commands/whoami.test.js +30 -0
- package/dist/config/consts.js +4 -5
- package/dist/config/index.js +1 -17
- package/dist/index.js +50 -80
- package/dist/services/app-bundle-files.js +117 -136
- package/dist/services/app-bundles.js +22 -41
- package/dist/services/app-channels.js +54 -77
- package/dist/services/app-devices.js +10 -25
- package/dist/services/apps.js +25 -41
- package/dist/services/authorization-service.js +4 -8
- package/dist/services/config.js +15 -28
- package/dist/services/organizations.js +18 -0
- package/dist/services/session-code.js +7 -22
- package/dist/services/sessions.js +13 -30
- package/dist/services/update.js +17 -55
- package/dist/services/users.js +11 -26
- package/dist/types/app-bundle-file.js +1 -2
- package/dist/types/app-bundle.js +1 -2
- package/dist/types/app-channel.js +1 -2
- package/dist/types/app-device.js +1 -2
- package/dist/types/app.js +1 -2
- package/dist/types/index.js +8 -23
- package/dist/types/npm-package.js +1 -2
- package/dist/types/organization.js +1 -0
- package/dist/types/session-code.js +1 -2
- package/dist/types/session.js +1 -2
- package/dist/types/user.js +1 -2
- package/dist/utils/buffer.js +12 -43
- package/dist/utils/error.js +24 -14
- package/dist/utils/file.js +22 -41
- package/dist/utils/hash.js +3 -39
- package/dist/utils/http-client.js +27 -53
- package/dist/utils/manifest.js +11 -24
- package/dist/utils/prompt.js +9 -26
- package/dist/utils/signature.js +3 -39
- package/dist/utils/userConfig.js +5 -9
- package/dist/utils/zip.js +11 -27
- package/package.json +23 -10
- package/dist/utils/ci.js +0 -7
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import userConfig from '../../../utils/userConfig.js';
|
|
4
|
+
import consola from 'consola';
|
|
5
|
+
import nock from 'nock';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import listChannelsCommand from './list.js';
|
|
8
|
+
// Mock dependencies
|
|
9
|
+
vi.mock('@/utils/userConfig.js');
|
|
10
|
+
vi.mock('@/services/authorization-service.js');
|
|
11
|
+
vi.mock('consola');
|
|
12
|
+
describe('apps-channels-list', () => {
|
|
13
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
14
|
+
const mockConsola = vi.mocked(consola);
|
|
15
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
19
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
20
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
21
|
+
vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
22
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
23
|
+
vi.spyOn(console, 'table').mockImplementation(() => { });
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
nock.cleanAll();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
it('should require authentication', async () => {
|
|
30
|
+
const appId = 'app-123';
|
|
31
|
+
const options = { appId };
|
|
32
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
|
|
33
|
+
await listChannelsCommand.action(options, undefined);
|
|
34
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
|
|
35
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
36
|
+
});
|
|
37
|
+
it('should require appId', async () => {
|
|
38
|
+
const options = {};
|
|
39
|
+
await listChannelsCommand.action(options, undefined);
|
|
40
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must provide an app ID.');
|
|
41
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
42
|
+
});
|
|
43
|
+
it('should list channels and display table format', async () => {
|
|
44
|
+
const appId = 'app-123';
|
|
45
|
+
const testToken = 'test-token';
|
|
46
|
+
const channels = [
|
|
47
|
+
{
|
|
48
|
+
id: 'channel-1',
|
|
49
|
+
name: 'production',
|
|
50
|
+
totalAppBundleLimit: 10,
|
|
51
|
+
appId,
|
|
52
|
+
createdAt: '2023-01-01T00:00:00Z',
|
|
53
|
+
updatedAt: '2023-01-01T00:00:00Z',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'channel-2',
|
|
57
|
+
name: 'staging',
|
|
58
|
+
totalAppBundleLimit: 5,
|
|
59
|
+
appId,
|
|
60
|
+
createdAt: '2023-01-02T00:00:00Z',
|
|
61
|
+
updatedAt: '2023-01-02T00:00:00Z',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const options = { appId };
|
|
65
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
66
|
+
.get(`/v1/apps/${appId}/channels`)
|
|
67
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
68
|
+
.reply(200, channels);
|
|
69
|
+
await listChannelsCommand.action(options, undefined);
|
|
70
|
+
expect(scope.isDone()).toBe(true);
|
|
71
|
+
expect(console.table).toHaveBeenCalledWith(channels);
|
|
72
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Channels retrieved successfully.');
|
|
73
|
+
});
|
|
74
|
+
it('should list channels with JSON format', async () => {
|
|
75
|
+
const appId = 'app-123';
|
|
76
|
+
const testToken = 'test-token';
|
|
77
|
+
const channels = [
|
|
78
|
+
{
|
|
79
|
+
id: 'channel-1',
|
|
80
|
+
name: 'production',
|
|
81
|
+
totalAppBundleLimit: 10,
|
|
82
|
+
appId,
|
|
83
|
+
createdAt: '2023-01-01T00:00:00Z',
|
|
84
|
+
updatedAt: '2023-01-01T00:00:00Z',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const options = { appId, json: true };
|
|
88
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
89
|
+
.get(`/v1/apps/${appId}/channels`)
|
|
90
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
91
|
+
.reply(200, channels);
|
|
92
|
+
await listChannelsCommand.action(options, undefined);
|
|
93
|
+
expect(scope.isDone()).toBe(true);
|
|
94
|
+
expect(console.log).toHaveBeenCalledWith(JSON.stringify(channels, null, 2));
|
|
95
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
it('should handle empty channels list', async () => {
|
|
98
|
+
const appId = 'app-123';
|
|
99
|
+
const testToken = 'test-token';
|
|
100
|
+
const options = { appId };
|
|
101
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
102
|
+
.get(`/v1/apps/${appId}/channels`)
|
|
103
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
104
|
+
.reply(200, []);
|
|
105
|
+
await listChannelsCommand.action(options, undefined);
|
|
106
|
+
expect(scope.isDone()).toBe(true);
|
|
107
|
+
expect(console.table).toHaveBeenCalledWith([]);
|
|
108
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Channels retrieved successfully.');
|
|
109
|
+
});
|
|
110
|
+
it('should handle API error', async () => {
|
|
111
|
+
const appId = 'app-123';
|
|
112
|
+
const testToken = 'test-token';
|
|
113
|
+
const options = { appId };
|
|
114
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
115
|
+
.get(`/v1/apps/${appId}/channels`)
|
|
116
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
117
|
+
.reply(500, { message: 'Internal server error' });
|
|
118
|
+
await listChannelsCommand.action(options, undefined);
|
|
119
|
+
expect(scope.isDone()).toBe(true);
|
|
120
|
+
expect(mockConsola.error).toHaveBeenCalled();
|
|
121
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -1,95 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
args: {
|
|
27
|
-
appId: {
|
|
28
|
-
type: 'string',
|
|
29
|
-
description: 'ID of the app.',
|
|
30
|
-
},
|
|
31
|
-
channelId: {
|
|
32
|
-
type: 'string',
|
|
33
|
-
description: 'ID of the channel.',
|
|
34
|
-
},
|
|
35
|
-
bundleLimit: {
|
|
36
|
-
type: 'string',
|
|
37
|
-
description: 'Maximum number of bundles that can be assigned to the channel. If more bundles are assigned, the oldest bundles will be automatically deleted.',
|
|
38
|
-
},
|
|
39
|
-
name: {
|
|
40
|
-
type: 'string',
|
|
41
|
-
description: 'Name of the channel.',
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
run: (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
-
if (!authorization_service_1.default.hasAuthorizationToken()) {
|
|
46
|
-
consola_1.default.error('You must be logged in to run this command.');
|
|
1
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
2
|
+
import consola from 'consola';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import appChannelsService from '../../../services/app-channels.js';
|
|
5
|
+
import appsService from '../../../services/apps.js';
|
|
6
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
7
|
+
import organizationsService from '../../../services/organizations.js';
|
|
8
|
+
import { getMessageFromUnknownError } from '../../../utils/error.js';
|
|
9
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
description: 'Update an existing app channel.',
|
|
12
|
+
options: defineOptions(z.object({
|
|
13
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
14
|
+
channelId: z.string().optional().describe('ID of the channel.'),
|
|
15
|
+
bundleLimit: z.coerce
|
|
16
|
+
.number()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Maximum number of bundles that can be assigned to the channel. If more bundles are assigned, the oldest bundles will be automatically deleted.'),
|
|
19
|
+
name: z.string().optional().describe('Name of the channel.'),
|
|
20
|
+
})),
|
|
21
|
+
action: async (options, args) => {
|
|
22
|
+
let { appId, channelId, bundleLimit, name } = options;
|
|
23
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
24
|
+
consola.error('You must be logged in to run this command.');
|
|
47
25
|
process.exit(1);
|
|
48
26
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Validate the bundle limit
|
|
54
|
-
let bundleLimit;
|
|
55
|
-
if (bundleLimitAsString) {
|
|
56
|
-
bundleLimit = parseInt(bundleLimitAsString, 10);
|
|
57
|
-
if (isNaN(bundleLimit)) {
|
|
58
|
-
consola_1.default.error('The bundle limit must be a number.');
|
|
27
|
+
if (!appId) {
|
|
28
|
+
const organizations = await organizationsService.findAll();
|
|
29
|
+
if (organizations.length === 0) {
|
|
30
|
+
consola.error('You must create an organization before updating a channel.');
|
|
59
31
|
process.exit(1);
|
|
60
32
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
33
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
34
|
+
const organizationId = await prompt('Select the organization of the app for which you want to update a channel.', {
|
|
35
|
+
type: 'select',
|
|
36
|
+
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
37
|
+
});
|
|
38
|
+
if (!organizationId) {
|
|
39
|
+
consola.error('You must select the organization of an app for which you want to update a channel.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const apps = await appsService.findAll({
|
|
43
|
+
organizationId,
|
|
44
|
+
});
|
|
64
45
|
if (!apps.length) {
|
|
65
|
-
|
|
46
|
+
consola.error('You must create an app before updating a channel.');
|
|
66
47
|
process.exit(1);
|
|
67
48
|
}
|
|
68
49
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
69
|
-
appId =
|
|
50
|
+
appId = await prompt('Which app do you want to update the channel for?', {
|
|
70
51
|
type: 'select',
|
|
71
52
|
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
72
53
|
});
|
|
73
54
|
}
|
|
74
55
|
if (!channelId) {
|
|
75
|
-
channelId =
|
|
56
|
+
channelId = await prompt('Enter the channel ID:', {
|
|
76
57
|
type: 'text',
|
|
77
58
|
});
|
|
78
59
|
}
|
|
79
60
|
// Update channel
|
|
80
61
|
try {
|
|
81
|
-
|
|
62
|
+
await appChannelsService.update({
|
|
82
63
|
appId,
|
|
83
64
|
appChannelId: channelId,
|
|
84
65
|
name,
|
|
85
66
|
totalAppBundleLimit: bundleLimit,
|
|
86
67
|
});
|
|
87
|
-
|
|
68
|
+
consola.success('Channel updated successfully.');
|
|
88
69
|
}
|
|
89
70
|
catch (error) {
|
|
90
|
-
const message =
|
|
91
|
-
|
|
71
|
+
const message = getMessageFromUnknownError(error);
|
|
72
|
+
consola.error(message);
|
|
92
73
|
process.exit(1);
|
|
93
74
|
}
|
|
94
|
-
}
|
|
75
|
+
},
|
|
95
76
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
4
|
+
import userConfig from '../../../utils/userConfig.js';
|
|
5
|
+
import consola from 'consola';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import updateChannelCommand from './update.js';
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@/utils/userConfig.js');
|
|
11
|
+
vi.mock('@/utils/prompt.js');
|
|
12
|
+
vi.mock('@/services/authorization-service.js');
|
|
13
|
+
vi.mock('consola');
|
|
14
|
+
describe('apps-channels-update', () => {
|
|
15
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
16
|
+
const mockPrompt = vi.mocked(prompt);
|
|
17
|
+
const mockConsola = vi.mocked(consola);
|
|
18
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
22
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
23
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
24
|
+
vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
nock.cleanAll();
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
it('should require authentication', async () => {
|
|
31
|
+
const appId = 'app-123';
|
|
32
|
+
const channelId = 'channel-456';
|
|
33
|
+
const options = { appId, channelId };
|
|
34
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
|
|
35
|
+
await updateChannelCommand.action(options, undefined);
|
|
36
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
|
|
37
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
38
|
+
});
|
|
39
|
+
it('should update channel with provided options', async () => {
|
|
40
|
+
const appId = 'app-123';
|
|
41
|
+
const channelId = 'channel-456';
|
|
42
|
+
const channelName = 'updated-production';
|
|
43
|
+
const bundleLimit = 15;
|
|
44
|
+
const testToken = 'test-token';
|
|
45
|
+
const options = { appId, channelId, name: channelName, bundleLimit };
|
|
46
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
47
|
+
.patch(`/v1/apps/${appId}/channels/${channelId}`, {
|
|
48
|
+
appId,
|
|
49
|
+
appChannelId: channelId,
|
|
50
|
+
name: channelName,
|
|
51
|
+
totalAppBundleLimit: bundleLimit,
|
|
52
|
+
})
|
|
53
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
54
|
+
.reply(200, { id: channelId, name: channelName });
|
|
55
|
+
await updateChannelCommand.action(options, undefined);
|
|
56
|
+
expect(scope.isDone()).toBe(true);
|
|
57
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Channel updated successfully.');
|
|
58
|
+
});
|
|
59
|
+
it('should prompt for app selection when appId not provided', async () => {
|
|
60
|
+
const orgId = 'org-1';
|
|
61
|
+
const appId = 'app-1';
|
|
62
|
+
const channelId = 'channel-456';
|
|
63
|
+
const testToken = 'test-token';
|
|
64
|
+
const organization = { id: orgId, name: 'Org 1' };
|
|
65
|
+
const app = { id: appId, name: 'App 1' };
|
|
66
|
+
const options = { channelId, bundleLimit: 10 };
|
|
67
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
68
|
+
.get('/v1/organizations')
|
|
69
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
70
|
+
.reply(200, [organization]);
|
|
71
|
+
const appsScope = nock(DEFAULT_API_BASE_URL)
|
|
72
|
+
.get('/v1/apps')
|
|
73
|
+
.query({ organizationId: orgId })
|
|
74
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
75
|
+
.reply(200, [app]);
|
|
76
|
+
const updateScope = nock(DEFAULT_API_BASE_URL)
|
|
77
|
+
.patch(`/v1/apps/${appId}/channels/${channelId}`)
|
|
78
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
79
|
+
.reply(200, { id: channelId });
|
|
80
|
+
mockPrompt
|
|
81
|
+
.mockResolvedValueOnce(orgId) // organization selection
|
|
82
|
+
.mockResolvedValueOnce(appId) // app selection
|
|
83
|
+
.mockResolvedValueOnce(channelId); // channel ID input
|
|
84
|
+
await updateChannelCommand.action(options, undefined);
|
|
85
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
86
|
+
expect(appsScope.isDone()).toBe(true);
|
|
87
|
+
expect(updateScope.isDone()).toBe(true);
|
|
88
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Channel updated successfully.');
|
|
89
|
+
});
|
|
90
|
+
it('should prompt for channelId when not provided', async () => {
|
|
91
|
+
const appId = 'app-123';
|
|
92
|
+
const channelId = 'channel-456';
|
|
93
|
+
const testToken = 'test-token';
|
|
94
|
+
const options = { appId, name: 'staging' };
|
|
95
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
96
|
+
.patch(`/v1/apps/${appId}/channels/${channelId}`)
|
|
97
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
98
|
+
.reply(200, { id: channelId });
|
|
99
|
+
mockPrompt.mockResolvedValueOnce(channelId); // channel ID input
|
|
100
|
+
await updateChannelCommand.action(options, undefined);
|
|
101
|
+
expect(scope.isDone()).toBe(true);
|
|
102
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the channel ID:', { type: 'text' });
|
|
103
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Channel updated successfully.');
|
|
104
|
+
});
|
|
105
|
+
it('should handle API error during update', async () => {
|
|
106
|
+
const appId = 'app-123';
|
|
107
|
+
const channelId = 'channel-456';
|
|
108
|
+
const testToken = 'test-token';
|
|
109
|
+
const options = { appId, channelId, name: 'updated-name' };
|
|
110
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
111
|
+
.patch(`/v1/apps/${appId}/channels/${channelId}`)
|
|
112
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
113
|
+
.reply(404, { message: 'Channel not found' });
|
|
114
|
+
await updateChannelCommand.action(options, undefined);
|
|
115
|
+
expect(scope.isDone()).toBe(true);
|
|
116
|
+
expect(mockConsola.error).toHaveBeenCalled();
|
|
117
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
118
|
+
});
|
|
119
|
+
it('should handle error when no organizations exist', async () => {
|
|
120
|
+
const testToken = 'test-token';
|
|
121
|
+
const options = { name: 'new-name' };
|
|
122
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
123
|
+
.get('/v1/organizations')
|
|
124
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
125
|
+
.reply(200, []);
|
|
126
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
127
|
+
throw new Error(`process.exit called with code ${code}`);
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
await updateChannelCommand.action(options, undefined);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
expect(error.message).toBe('process.exit called with code 1');
|
|
134
|
+
}
|
|
135
|
+
expect(scope.isDone()).toBe(true);
|
|
136
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must create an organization before updating a channel.');
|
|
137
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
2
|
+
import consola from 'consola';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import appsService from '../../services/apps.js';
|
|
5
|
+
import organizationsService from '../../services/organizations.js';
|
|
6
|
+
import { getMessageFromUnknownError } from '../../utils/error.js';
|
|
7
|
+
import { prompt } from '../../utils/prompt.js';
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
description: 'Create a new app.',
|
|
10
|
+
options: defineOptions(z.object({
|
|
11
|
+
name: z.string().optional().describe('Name of the app.'),
|
|
12
|
+
organizationId: z.string().optional().describe('ID of the organization to create the app in.'),
|
|
13
|
+
})),
|
|
14
|
+
action: async (options, args) => {
|
|
15
|
+
let { name, organizationId } = options;
|
|
16
|
+
if (!organizationId) {
|
|
17
|
+
const organizations = await organizationsService.findAll();
|
|
18
|
+
if (organizations.length === 0) {
|
|
19
|
+
consola.error('You must create an organization before creating an app.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
23
|
+
organizationId = await prompt('Which organization do you want to create the app in?', {
|
|
24
|
+
type: 'select',
|
|
25
|
+
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
26
|
+
});
|
|
27
|
+
if (!organizationId) {
|
|
28
|
+
consola.error('You must select an organization to create the app in.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
32
|
if (!name) {
|
|
33
|
-
name =
|
|
33
|
+
name = await prompt('Enter the name of the app:', { type: 'text' });
|
|
34
34
|
}
|
|
35
35
|
try {
|
|
36
|
-
const response =
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const response = await appsService.create({ name, organizationId });
|
|
37
|
+
consola.success('App created successfully.');
|
|
38
|
+
consola.info(`App ID: ${response.id}`);
|
|
39
39
|
}
|
|
40
40
|
catch (error) {
|
|
41
|
-
const message =
|
|
42
|
-
|
|
41
|
+
const message = getMessageFromUnknownError(error);
|
|
42
|
+
consola.error(message);
|
|
43
43
|
process.exit(1);
|
|
44
44
|
}
|
|
45
|
-
}
|
|
45
|
+
},
|
|
46
46
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../services/authorization-service.js';
|
|
3
|
+
import { prompt } from '../../utils/prompt.js';
|
|
4
|
+
import userConfig from '../../utils/userConfig.js';
|
|
5
|
+
import consola from 'consola';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import createAppCommand from './create.js';
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@/utils/userConfig.js');
|
|
11
|
+
vi.mock('@/utils/prompt.js');
|
|
12
|
+
vi.mock('@/services/authorization-service.js');
|
|
13
|
+
vi.mock('consola');
|
|
14
|
+
describe('apps-create', () => {
|
|
15
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
16
|
+
const mockPrompt = vi.mocked(prompt);
|
|
17
|
+
const mockConsola = vi.mocked(consola);
|
|
18
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
22
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
23
|
+
vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
nock.cleanAll();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
it('should create app with provided options', async () => {
|
|
30
|
+
const appName = 'Test App';
|
|
31
|
+
const organizationId = 'org-123';
|
|
32
|
+
const appId = 'app-456';
|
|
33
|
+
const testToken = 'test-token';
|
|
34
|
+
const options = { name: appName, organizationId };
|
|
35
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
36
|
+
.post(`/v1/apps?organizationId=${organizationId}`, { name: appName })
|
|
37
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
38
|
+
.reply(201, { id: appId, name: appName });
|
|
39
|
+
await createAppCommand.action(options, undefined);
|
|
40
|
+
expect(scope.isDone()).toBe(true);
|
|
41
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App created successfully.');
|
|
42
|
+
expect(mockConsola.info).toHaveBeenCalledWith(`App ID: ${appId}`);
|
|
43
|
+
});
|
|
44
|
+
it('should prompt for organization when not provided', async () => {
|
|
45
|
+
const appName = 'Test App';
|
|
46
|
+
const orgId1 = 'org-1';
|
|
47
|
+
const orgId2 = 'org-2';
|
|
48
|
+
const appId = 'app-456';
|
|
49
|
+
const testToken = 'test-token';
|
|
50
|
+
const organizations = [
|
|
51
|
+
{ id: orgId1, name: 'Org 1' },
|
|
52
|
+
{ id: orgId2, name: 'Org 2' },
|
|
53
|
+
];
|
|
54
|
+
const options = { name: appName };
|
|
55
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
56
|
+
.get('/v1/organizations')
|
|
57
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
58
|
+
.reply(200, organizations);
|
|
59
|
+
const createScope = nock(DEFAULT_API_BASE_URL)
|
|
60
|
+
.post(`/v1/apps?organizationId=${orgId1}`, { name: appName })
|
|
61
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
62
|
+
.reply(201, { id: appId, name: appName });
|
|
63
|
+
mockPrompt.mockResolvedValueOnce(orgId1);
|
|
64
|
+
await createAppCommand.action(options, undefined);
|
|
65
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
66
|
+
expect(createScope.isDone()).toBe(true);
|
|
67
|
+
expect(mockPrompt).toHaveBeenCalledWith('Which organization do you want to create the app in?', {
|
|
68
|
+
type: 'select',
|
|
69
|
+
options: [
|
|
70
|
+
{ label: 'Org 1', value: orgId1 },
|
|
71
|
+
{ label: 'Org 2', value: orgId2 },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it('should prompt for app name when not provided', async () => {
|
|
76
|
+
const organizationId = 'org-123';
|
|
77
|
+
const promptedAppName = 'Prompted App';
|
|
78
|
+
const appId = 'app-456';
|
|
79
|
+
const testToken = 'test-token';
|
|
80
|
+
const options = { organizationId };
|
|
81
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
82
|
+
.post(`/v1/apps?organizationId=${organizationId}`, { name: promptedAppName })
|
|
83
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
84
|
+
.reply(201, { id: appId, name: promptedAppName });
|
|
85
|
+
mockPrompt.mockResolvedValueOnce(promptedAppName);
|
|
86
|
+
await createAppCommand.action(options, undefined);
|
|
87
|
+
expect(scope.isDone()).toBe(true);
|
|
88
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the name of the app:', { type: 'text' });
|
|
89
|
+
});
|
|
90
|
+
it('should handle error when no organizations exist', async () => {
|
|
91
|
+
const appName = 'Test App';
|
|
92
|
+
const testToken = 'test-token';
|
|
93
|
+
const options = { name: appName };
|
|
94
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
95
|
+
.get('/v1/organizations')
|
|
96
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
97
|
+
.reply(200, []);
|
|
98
|
+
await createAppCommand.action(options, undefined);
|
|
99
|
+
expect(scope.isDone()).toBe(true);
|
|
100
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must create an organization before creating an app.');
|
|
101
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
102
|
+
});
|
|
103
|
+
it('should handle API error during creation', async () => {
|
|
104
|
+
const appName = 'Test App';
|
|
105
|
+
const organizationId = 'org-123';
|
|
106
|
+
const testToken = 'test-token';
|
|
107
|
+
const options = { name: appName, organizationId };
|
|
108
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
109
|
+
.post(`/v1/apps?organizationId=${organizationId}`, { name: appName })
|
|
110
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
111
|
+
.reply(400, { message: 'App name already exists' });
|
|
112
|
+
await createAppCommand.action(options, undefined);
|
|
113
|
+
expect(scope.isDone()).toBe(true);
|
|
114
|
+
expect(mockConsola.error).toHaveBeenCalled();
|
|
115
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
116
|
+
});
|
|
117
|
+
});
|