@capawesome/cli 1.14.0 → 2.0.1
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 +34 -0
- package/README.md +7 -3
- package/dist/commands/apps/bundles/create.js +206 -239
- package/dist/commands/apps/bundles/create.test.js +276 -0
- package/dist/commands/apps/bundles/delete.js +35 -60
- package/dist/commands/apps/bundles/delete.test.js +139 -0
- package/dist/commands/apps/bundles/update.js +61 -89
- package/dist/commands/apps/bundles/update.test.js +141 -0
- package/dist/commands/apps/channels/create.js +45 -75
- package/dist/commands/apps/channels/create.test.js +119 -0
- package/dist/commands/apps/channels/delete.js +46 -69
- package/dist/commands/apps/channels/delete.test.js +141 -0
- package/dist/commands/apps/channels/get.js +52 -94
- package/dist/commands/apps/channels/get.test.js +135 -0
- package/dist/commands/apps/channels/list.js +37 -82
- package/dist/commands/apps/channels/list.test.js +121 -0
- package/dist/commands/apps/channels/update.js +39 -83
- package/dist/commands/apps/channels/update.test.js +138 -0
- package/dist/commands/apps/create.js +28 -53
- package/dist/commands/apps/create.test.js +117 -0
- package/dist/commands/apps/delete.js +29 -50
- package/dist/commands/apps/delete.test.js +120 -0
- package/dist/commands/apps/devices/delete.js +35 -60
- package/dist/commands/apps/devices/delete.test.js +139 -0
- package/dist/commands/doctor.js +12 -29
- package/dist/commands/doctor.test.js +52 -0
- package/dist/commands/login.js +50 -71
- package/dist/commands/login.test.js +116 -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/organizations/create.js +25 -0
- package/dist/commands/organizations/create.test.js +80 -0
- package/dist/commands/whoami.js +20 -31
- 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 +59 -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 -43
- package/dist/services/authorization-service.js +4 -8
- package/dist/services/config.js +15 -28
- package/dist/services/organizations.js +19 -26
- 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 -24
- package/dist/types/npm-package.js +1 -2
- package/dist/types/organization.js +1 -2
- 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/private-key.js +23 -0
- package/dist/utils/prompt.js +9 -26
- package/dist/utils/signature.js +3 -39
- package/dist/utils/user-config.js +12 -0
- package/dist/utils/zip.js +11 -27
- package/package.json +22 -9
- package/dist/utils/ci.js +0 -7
- package/dist/utils/userConfig.js +0 -16
|
@@ -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/user-config.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/user-config.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
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
24
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
25
|
+
throw new Error(`Process exited with code ${code}`);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
nock.cleanAll();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
it('should create app with provided options', async () => {
|
|
33
|
+
const appName = 'Test App';
|
|
34
|
+
const organizationId = 'org-123';
|
|
35
|
+
const appId = 'app-456';
|
|
36
|
+
const testToken = 'test-token';
|
|
37
|
+
const options = { name: appName, organizationId };
|
|
38
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
39
|
+
.post(`/v1/apps?organizationId=${organizationId}`, { name: appName })
|
|
40
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
41
|
+
.reply(201, { id: appId, name: appName });
|
|
42
|
+
await createAppCommand.action(options, undefined);
|
|
43
|
+
expect(scope.isDone()).toBe(true);
|
|
44
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App created successfully.');
|
|
45
|
+
expect(mockConsola.info).toHaveBeenCalledWith(`App ID: ${appId}`);
|
|
46
|
+
});
|
|
47
|
+
it('should prompt for organization when not provided', async () => {
|
|
48
|
+
const appName = 'Test App';
|
|
49
|
+
const orgId1 = 'org-1';
|
|
50
|
+
const orgId2 = 'org-2';
|
|
51
|
+
const appId = 'app-456';
|
|
52
|
+
const testToken = 'test-token';
|
|
53
|
+
const organizations = [
|
|
54
|
+
{ id: orgId1, name: 'Org 1' },
|
|
55
|
+
{ id: orgId2, name: 'Org 2' },
|
|
56
|
+
];
|
|
57
|
+
const options = { name: appName };
|
|
58
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
59
|
+
.get('/v1/organizations')
|
|
60
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
61
|
+
.reply(200, organizations);
|
|
62
|
+
const createScope = nock(DEFAULT_API_BASE_URL)
|
|
63
|
+
.post(`/v1/apps?organizationId=${orgId1}`, { name: appName })
|
|
64
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
65
|
+
.reply(201, { id: appId, name: appName });
|
|
66
|
+
mockPrompt.mockResolvedValueOnce(orgId1);
|
|
67
|
+
await createAppCommand.action(options, undefined);
|
|
68
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
69
|
+
expect(createScope.isDone()).toBe(true);
|
|
70
|
+
expect(mockPrompt).toHaveBeenCalledWith('Which organization do you want to create the app in?', {
|
|
71
|
+
type: 'select',
|
|
72
|
+
options: [
|
|
73
|
+
{ label: 'Org 1', value: orgId1 },
|
|
74
|
+
{ label: 'Org 2', value: orgId2 },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it('should prompt for app name when not provided', async () => {
|
|
79
|
+
const organizationId = 'org-123';
|
|
80
|
+
const promptedAppName = 'Prompted App';
|
|
81
|
+
const appId = 'app-456';
|
|
82
|
+
const testToken = 'test-token';
|
|
83
|
+
const options = { organizationId };
|
|
84
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
85
|
+
.post(`/v1/apps?organizationId=${organizationId}`, { name: promptedAppName })
|
|
86
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
87
|
+
.reply(201, { id: appId, name: promptedAppName });
|
|
88
|
+
mockPrompt.mockResolvedValueOnce(promptedAppName);
|
|
89
|
+
await createAppCommand.action(options, undefined);
|
|
90
|
+
expect(scope.isDone()).toBe(true);
|
|
91
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the name of the app:', { type: 'text' });
|
|
92
|
+
});
|
|
93
|
+
it('should handle error when no organizations exist', async () => {
|
|
94
|
+
const appName = 'Test App';
|
|
95
|
+
const testToken = 'test-token';
|
|
96
|
+
const options = { name: appName };
|
|
97
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
98
|
+
.get('/v1/organizations')
|
|
99
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
100
|
+
.reply(200, []);
|
|
101
|
+
await expect(createAppCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
102
|
+
expect(scope.isDone()).toBe(true);
|
|
103
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must create an organization before creating an app.');
|
|
104
|
+
});
|
|
105
|
+
it('should handle API error during creation', async () => {
|
|
106
|
+
const appName = 'Test App';
|
|
107
|
+
const organizationId = 'org-123';
|
|
108
|
+
const testToken = 'test-token';
|
|
109
|
+
const options = { name: appName, organizationId };
|
|
110
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
111
|
+
.post(`/v1/apps?organizationId=${organizationId}`, { name: appName })
|
|
112
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
113
|
+
.reply(400, { message: 'App name already exists' });
|
|
114
|
+
await expect(createAppCommand.action(options, undefined)).rejects.toThrow();
|
|
115
|
+
expect(scope.isDone()).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,77 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const error_1 = require("../../utils/error");
|
|
20
|
-
const prompt_1 = require("../../utils/prompt");
|
|
21
|
-
exports.default = (0, citty_1.defineCommand)({
|
|
22
|
-
meta: {
|
|
23
|
-
description: 'Delete an app.',
|
|
24
|
-
},
|
|
25
|
-
args: {
|
|
26
|
-
appId: {
|
|
27
|
-
type: 'string',
|
|
28
|
-
description: 'ID of the app.',
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
run: (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
32
|
-
let appId = ctx.args.appId;
|
|
1
|
+
import appsService from '../../services/apps.js';
|
|
2
|
+
import authorizationService from '../../services/authorization-service.js';
|
|
3
|
+
import organizationsService from '../../services/organizations.js';
|
|
4
|
+
import { prompt } from '../../utils/prompt.js';
|
|
5
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
6
|
+
import consola from 'consola';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
description: 'Delete an app.',
|
|
10
|
+
options: defineOptions(z.object({
|
|
11
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
12
|
+
})),
|
|
13
|
+
action: async (options, args) => {
|
|
14
|
+
let { appId } = options;
|
|
15
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
16
|
+
consola.error('You must be logged in to run this command.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
33
19
|
if (!appId) {
|
|
34
|
-
const organizations =
|
|
20
|
+
const organizations = await organizationsService.findAll();
|
|
35
21
|
if (organizations.length === 0) {
|
|
36
|
-
|
|
22
|
+
consola.error('You must create an organization before deleting an app.');
|
|
37
23
|
process.exit(1);
|
|
38
24
|
}
|
|
39
25
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
40
|
-
const organizationId =
|
|
26
|
+
const organizationId = await prompt('Which organization do you want to delete the app from?', {
|
|
41
27
|
type: 'select',
|
|
42
28
|
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
43
29
|
});
|
|
44
30
|
if (!organizationId) {
|
|
45
|
-
|
|
31
|
+
consola.error('You must select an organization to delete the app from.');
|
|
46
32
|
process.exit(1);
|
|
47
33
|
}
|
|
48
|
-
const apps =
|
|
34
|
+
const apps = await appsService.findAll({
|
|
49
35
|
organizationId,
|
|
50
36
|
});
|
|
51
37
|
if (!apps.length) {
|
|
52
|
-
|
|
38
|
+
consola.error('You must create an app before deleting it.');
|
|
53
39
|
process.exit(1);
|
|
54
40
|
}
|
|
55
41
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
56
|
-
appId =
|
|
42
|
+
appId = await prompt('Which app do you want to delete?', {
|
|
57
43
|
type: 'select',
|
|
58
44
|
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
59
45
|
});
|
|
60
46
|
}
|
|
61
|
-
const confirmed =
|
|
47
|
+
const confirmed = await prompt('Are you sure you want to delete this app?', {
|
|
62
48
|
type: 'confirm',
|
|
63
49
|
});
|
|
64
50
|
if (!confirmed) {
|
|
65
51
|
return;
|
|
66
52
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
const message = (0, error_1.getMessageFromUnknownError)(error);
|
|
73
|
-
consola_1.default.error(message);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
}),
|
|
53
|
+
await appsService.delete({ id: appId });
|
|
54
|
+
consola.success('App deleted successfully.');
|
|
55
|
+
},
|
|
77
56
|
});
|
|
@@ -0,0 +1,120 @@
|
|
|
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/user-config.js';
|
|
5
|
+
import consola from 'consola';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import deleteAppCommand from './delete.js';
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@/utils/user-config.js');
|
|
11
|
+
vi.mock('@/utils/prompt.js');
|
|
12
|
+
vi.mock('@/services/authorization-service.js');
|
|
13
|
+
vi.mock('consola');
|
|
14
|
+
describe('apps-delete', () => {
|
|
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
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
24
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
25
|
+
throw new Error(`Process exited with code ${code}`);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
nock.cleanAll();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
it('should delete app with provided appId after confirmation', async () => {
|
|
33
|
+
const appId = 'app-123';
|
|
34
|
+
const testToken = 'test-token';
|
|
35
|
+
const options = { appId };
|
|
36
|
+
mockPrompt.mockResolvedValueOnce(true); // confirmation
|
|
37
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
38
|
+
.delete(`/v1/apps/${appId}`)
|
|
39
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
40
|
+
.reply(200);
|
|
41
|
+
await deleteAppCommand.action(options, undefined);
|
|
42
|
+
expect(scope.isDone()).toBe(true);
|
|
43
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to delete this app?', {
|
|
44
|
+
type: 'confirm',
|
|
45
|
+
});
|
|
46
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App deleted successfully.');
|
|
47
|
+
});
|
|
48
|
+
it('should not delete app when confirmation is declined', async () => {
|
|
49
|
+
const appId = 'app-123';
|
|
50
|
+
const options = { appId };
|
|
51
|
+
mockPrompt.mockResolvedValueOnce(false); // declined confirmation
|
|
52
|
+
await deleteAppCommand.action(options, undefined);
|
|
53
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to delete this app?', {
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
});
|
|
56
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('should prompt for app selection when appId not provided', async () => {
|
|
59
|
+
const orgId = 'org-1';
|
|
60
|
+
const appId = 'app-1';
|
|
61
|
+
const testToken = 'test-token';
|
|
62
|
+
const organization = { id: orgId, name: 'Org 1' };
|
|
63
|
+
const app = { id: appId, name: 'App 1' };
|
|
64
|
+
const options = {};
|
|
65
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
66
|
+
.get('/v1/organizations')
|
|
67
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
68
|
+
.reply(200, [organization]);
|
|
69
|
+
const appsScope = nock(DEFAULT_API_BASE_URL)
|
|
70
|
+
.get('/v1/apps')
|
|
71
|
+
.query({ organizationId: orgId })
|
|
72
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
73
|
+
.reply(200, [app]);
|
|
74
|
+
const deleteScope = nock(DEFAULT_API_BASE_URL)
|
|
75
|
+
.delete(`/v1/apps/${appId}`)
|
|
76
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
77
|
+
.reply(200);
|
|
78
|
+
mockPrompt
|
|
79
|
+
.mockResolvedValueOnce(orgId) // organization selection
|
|
80
|
+
.mockResolvedValueOnce(appId) // app selection
|
|
81
|
+
.mockResolvedValueOnce(true); // confirmation
|
|
82
|
+
await deleteAppCommand.action(options, undefined);
|
|
83
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
84
|
+
expect(appsScope.isDone()).toBe(true);
|
|
85
|
+
expect(deleteScope.isDone()).toBe(true);
|
|
86
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App deleted successfully.');
|
|
87
|
+
});
|
|
88
|
+
it('should handle error when no organizations exist', async () => {
|
|
89
|
+
const testToken = 'test-token';
|
|
90
|
+
const options = {};
|
|
91
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
92
|
+
.get('/v1/organizations')
|
|
93
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
94
|
+
.reply(200, []);
|
|
95
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
96
|
+
throw new Error(`process.exit called with code ${code}`);
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
await deleteAppCommand.action(options, undefined);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
expect(error.message).toBe('process.exit called with code 1');
|
|
103
|
+
}
|
|
104
|
+
expect(scope.isDone()).toBe(true);
|
|
105
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must create an organization before deleting an app.');
|
|
106
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
107
|
+
});
|
|
108
|
+
it('should handle API error during deletion', async () => {
|
|
109
|
+
const appId = 'app-123';
|
|
110
|
+
const testToken = 'test-token';
|
|
111
|
+
const options = { appId };
|
|
112
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
113
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
114
|
+
.delete(`/v1/apps/${appId}`)
|
|
115
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
116
|
+
.reply(404, { message: 'App not found' });
|
|
117
|
+
await expect(deleteAppCommand.action(options, undefined)).rejects.toThrow();
|
|
118
|
+
expect(scope.isDone()).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -1,91 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const prompt_1 = require("../../../utils/prompt");
|
|
22
|
-
exports.default = (0, citty_1.defineCommand)({
|
|
23
|
-
meta: {
|
|
24
|
-
description: 'Delete an app device.',
|
|
25
|
-
},
|
|
26
|
-
args: {
|
|
27
|
-
appId: {
|
|
28
|
-
type: 'string',
|
|
29
|
-
description: 'ID of the app.',
|
|
30
|
-
},
|
|
31
|
-
deviceId: {
|
|
32
|
-
type: 'string',
|
|
33
|
-
description: 'ID of the device.',
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
run: (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
|
-
let appId = ctx.args.appId;
|
|
1
|
+
import appDevicesService from '../../../services/app-devices.js';
|
|
2
|
+
import appsService from '../../../services/apps.js';
|
|
3
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
4
|
+
import organizationsService from '../../../services/organizations.js';
|
|
5
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
6
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
7
|
+
import consola from 'consola';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: 'Delete an app device.',
|
|
11
|
+
options: defineOptions(z.object({
|
|
12
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
13
|
+
deviceId: z.string().optional().describe('ID of the device.'),
|
|
14
|
+
})),
|
|
15
|
+
action: async (options, args) => {
|
|
16
|
+
let { appId, deviceId } = options;
|
|
17
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
18
|
+
consola.error('You must be logged in to run this command.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
38
21
|
if (!appId) {
|
|
39
|
-
const organizations =
|
|
22
|
+
const organizations = await organizationsService.findAll();
|
|
40
23
|
if (organizations.length === 0) {
|
|
41
|
-
|
|
24
|
+
consola.error('You must create an organization before deleting a device.');
|
|
42
25
|
process.exit(1);
|
|
43
26
|
}
|
|
44
27
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
45
|
-
const organizationId =
|
|
28
|
+
const organizationId = await prompt('Select the organization of the app from which you want to delete a device.', {
|
|
46
29
|
type: 'select',
|
|
47
30
|
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
48
31
|
});
|
|
49
32
|
if (!organizationId) {
|
|
50
|
-
|
|
33
|
+
consola.error('You must select the organization of an app from which you want to delete a device.');
|
|
51
34
|
process.exit(1);
|
|
52
35
|
}
|
|
53
|
-
const apps =
|
|
36
|
+
const apps = await appsService.findAll({
|
|
54
37
|
organizationId,
|
|
55
38
|
});
|
|
56
39
|
if (!apps.length) {
|
|
57
|
-
|
|
40
|
+
consola.error('You must create an app before deleting a device.');
|
|
58
41
|
process.exit(1);
|
|
59
42
|
}
|
|
60
43
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
61
|
-
appId =
|
|
44
|
+
appId = await prompt('Which app do you want to delete the device from?', {
|
|
62
45
|
type: 'select',
|
|
63
46
|
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
64
47
|
});
|
|
65
48
|
}
|
|
66
|
-
let deviceId = ctx.args.deviceId;
|
|
67
49
|
if (!deviceId) {
|
|
68
|
-
deviceId =
|
|
50
|
+
deviceId = await prompt('Enter the device ID:', {
|
|
69
51
|
type: 'text',
|
|
70
52
|
});
|
|
71
53
|
}
|
|
72
|
-
const confirmed =
|
|
54
|
+
const confirmed = await prompt('Are you sure you want to delete this device?', {
|
|
73
55
|
type: 'confirm',
|
|
74
56
|
});
|
|
75
57
|
if (!confirmed) {
|
|
76
58
|
return;
|
|
77
59
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
const message = (0, error_1.getMessageFromUnknownError)(error);
|
|
87
|
-
consola_1.default.error(message);
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
}),
|
|
60
|
+
await appDevicesService.delete({
|
|
61
|
+
appId,
|
|
62
|
+
deviceId,
|
|
63
|
+
});
|
|
64
|
+
consola.success('Device deleted successfully.');
|
|
65
|
+
},
|
|
91
66
|
});
|
|
@@ -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/user-config.js';
|
|
5
|
+
import consola from 'consola';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import deleteDeviceCommand from './delete.js';
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@/utils/user-config.js');
|
|
11
|
+
vi.mock('@/utils/prompt.js');
|
|
12
|
+
vi.mock('@/services/authorization-service.js');
|
|
13
|
+
vi.mock('consola');
|
|
14
|
+
describe('apps-devices-delete', () => {
|
|
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
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
24
|
+
vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
nock.cleanAll();
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
it('should delete device with provided appId and deviceId after confirmation', async () => {
|
|
31
|
+
const appId = 'app-123';
|
|
32
|
+
const deviceId = 'device-456';
|
|
33
|
+
const testToken = 'test-token';
|
|
34
|
+
const options = { appId, deviceId };
|
|
35
|
+
mockPrompt.mockResolvedValueOnce(true); // confirmation
|
|
36
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
37
|
+
.delete(`/v1/apps/${appId}/devices/${deviceId}`)
|
|
38
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
39
|
+
.reply(200);
|
|
40
|
+
await deleteDeviceCommand.action(options, undefined);
|
|
41
|
+
expect(scope.isDone()).toBe(true);
|
|
42
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to delete this device?', {
|
|
43
|
+
type: 'confirm',
|
|
44
|
+
});
|
|
45
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Device deleted successfully.');
|
|
46
|
+
});
|
|
47
|
+
it('should not delete device when confirmation is declined', async () => {
|
|
48
|
+
const appId = 'app-123';
|
|
49
|
+
const deviceId = 'device-456';
|
|
50
|
+
const options = { appId, deviceId };
|
|
51
|
+
mockPrompt.mockResolvedValueOnce(false); // declined confirmation
|
|
52
|
+
await deleteDeviceCommand.action(options, undefined);
|
|
53
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to delete this device?', {
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
});
|
|
56
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('should prompt for app selection when appId not provided', async () => {
|
|
59
|
+
const orgId = 'org-1';
|
|
60
|
+
const appId = 'app-1';
|
|
61
|
+
const deviceId = 'device-456';
|
|
62
|
+
const testToken = 'test-token';
|
|
63
|
+
const organization = { id: orgId, name: 'Org 1' };
|
|
64
|
+
const app = { id: appId, name: 'App 1' };
|
|
65
|
+
const options = { deviceId };
|
|
66
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
67
|
+
.get('/v1/organizations')
|
|
68
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
69
|
+
.reply(200, [organization]);
|
|
70
|
+
const appsScope = nock(DEFAULT_API_BASE_URL)
|
|
71
|
+
.get('/v1/apps')
|
|
72
|
+
.query({ organizationId: orgId })
|
|
73
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
74
|
+
.reply(200, [app]);
|
|
75
|
+
const deleteScope = nock(DEFAULT_API_BASE_URL)
|
|
76
|
+
.delete(`/v1/apps/${appId}/devices/${deviceId}`)
|
|
77
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
78
|
+
.reply(200);
|
|
79
|
+
mockPrompt
|
|
80
|
+
.mockResolvedValueOnce(orgId) // organization selection
|
|
81
|
+
.mockResolvedValueOnce(appId) // app selection
|
|
82
|
+
.mockResolvedValueOnce(true); // confirmation
|
|
83
|
+
await deleteDeviceCommand.action(options, undefined);
|
|
84
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
85
|
+
expect(appsScope.isDone()).toBe(true);
|
|
86
|
+
expect(deleteScope.isDone()).toBe(true);
|
|
87
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Device deleted successfully.');
|
|
88
|
+
});
|
|
89
|
+
it('should prompt for deviceId when not provided', async () => {
|
|
90
|
+
const appId = 'app-123';
|
|
91
|
+
const deviceId = 'device-456';
|
|
92
|
+
const testToken = 'test-token';
|
|
93
|
+
const options = { appId };
|
|
94
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
95
|
+
.delete(`/v1/apps/${appId}/devices/${deviceId}`)
|
|
96
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
97
|
+
.reply(200);
|
|
98
|
+
mockPrompt
|
|
99
|
+
.mockResolvedValueOnce(deviceId) // device ID input
|
|
100
|
+
.mockResolvedValueOnce(true); // confirmation
|
|
101
|
+
await deleteDeviceCommand.action(options, undefined);
|
|
102
|
+
expect(scope.isDone()).toBe(true);
|
|
103
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the device ID:', { type: 'text' });
|
|
104
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Device deleted successfully.');
|
|
105
|
+
});
|
|
106
|
+
it('should handle error when no organizations exist', async () => {
|
|
107
|
+
const testToken = 'test-token';
|
|
108
|
+
const options = {};
|
|
109
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
110
|
+
.get('/v1/organizations')
|
|
111
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
112
|
+
.reply(200, []);
|
|
113
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
114
|
+
throw new Error(`process.exit called with code ${code}`);
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
await deleteDeviceCommand.action(options, undefined);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
expect(error.message).toBe('process.exit called with code 1');
|
|
121
|
+
}
|
|
122
|
+
expect(scope.isDone()).toBe(true);
|
|
123
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must create an organization before deleting a device.');
|
|
124
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
125
|
+
});
|
|
126
|
+
it('should handle API error during deletion', async () => {
|
|
127
|
+
const appId = 'app-123';
|
|
128
|
+
const deviceId = 'device-456';
|
|
129
|
+
const testToken = 'test-token';
|
|
130
|
+
const options = { appId, deviceId };
|
|
131
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
132
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
133
|
+
.delete(`/v1/apps/${appId}/devices/${deviceId}`)
|
|
134
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
135
|
+
.reply(404, { message: 'Device not found' });
|
|
136
|
+
await expect(deleteDeviceCommand.action(options, undefined)).rejects.toThrow();
|
|
137
|
+
expect(scope.isDone()).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|