kitstore-cli 1.0.0
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/.env.test +4 -0
- package/.eslintrc.js +29 -0
- package/dist/__tests__/commands/init.test.js +76 -0
- package/dist/__tests__/commands/install.test.js +422 -0
- package/dist/__tests__/commands/list.test.js +173 -0
- package/dist/__tests__/commands/login.test.js +281 -0
- package/dist/__tests__/commands/rule-check.test.js +72 -0
- package/dist/__tests__/commands/search.test.js +175 -0
- package/dist/__tests__/commands/upload.test.js +367 -0
- package/dist/__tests__/config.test.js +179 -0
- package/dist/__tests__/setup.js +8 -0
- package/dist/api/client.js +18 -0
- package/dist/api/generated/api.js +912 -0
- package/dist/api/generated/base.js +48 -0
- package/dist/api/generated/common.js +108 -0
- package/dist/api/generated/configuration.js +48 -0
- package/dist/api/generated/index.js +31 -0
- package/dist/commands/init.js +79 -0
- package/dist/commands/install.js +150 -0
- package/dist/commands/list.js +70 -0
- package/dist/commands/login.js +64 -0
- package/dist/commands/rule-check.js +81 -0
- package/dist/commands/search.js +59 -0
- package/dist/commands/upload.js +138 -0
- package/dist/config.js +84 -0
- package/dist/index.js +71 -0
- package/e2e/install.e2e.test.ts +237 -0
- package/e2e/integration.e2e.test.ts +346 -0
- package/e2e/login.e2e.test.ts +188 -0
- package/jest.config.js +24 -0
- package/openapitools.json +7 -0
- package/package.json +41 -0
- package/src/__tests__/commands/init.test.ts +52 -0
- package/src/__tests__/commands/install.test.ts +449 -0
- package/src/__tests__/commands/list.test.ts +164 -0
- package/src/__tests__/commands/login.test.ts +293 -0
- package/src/__tests__/commands/rule-check.test.ts +52 -0
- package/src/__tests__/commands/search.test.ts +168 -0
- package/src/__tests__/commands/upload.test.ts +404 -0
- package/src/__tests__/config.test.ts +181 -0
- package/src/__tests__/setup.ts +11 -0
- package/src/api/client.ts +20 -0
- package/src/api/generated/.openapi-generator/FILES +17 -0
- package/src/api/generated/.openapi-generator/VERSION +1 -0
- package/src/api/generated/.openapi-generator-ignore +23 -0
- package/src/api/generated/api.ts +1171 -0
- package/src/api/generated/base.ts +62 -0
- package/src/api/generated/common.ts +113 -0
- package/src/api/generated/configuration.ts +121 -0
- package/src/api/generated/docs/AuthApi.md +158 -0
- package/src/api/generated/docs/AuthResponseDto.md +22 -0
- package/src/api/generated/docs/AuthUserDto.md +24 -0
- package/src/api/generated/docs/HealthApi.md +183 -0
- package/src/api/generated/docs/LoginDto.md +22 -0
- package/src/api/generated/docs/RegisterDto.md +24 -0
- package/src/api/generated/docs/RuleAuthorDto.md +22 -0
- package/src/api/generated/docs/RuleResponseDto.md +36 -0
- package/src/api/generated/docs/RulesApi.md +289 -0
- package/src/api/generated/git_push.sh +57 -0
- package/src/api/generated/index.ts +18 -0
- package/src/commands/init.ts +46 -0
- package/src/commands/install.ts +129 -0
- package/src/commands/list.ts +71 -0
- package/src/commands/login.ts +65 -0
- package/src/commands/rule-check.ts +49 -0
- package/src/commands/search.ts +66 -0
- package/src/commands/upload.ts +117 -0
- package/src/config.ts +66 -0
- package/src/index.ts +79 -0
- package/test-cli-config.js +118 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const search_1 = require("../../commands/search");
|
|
37
|
+
const config = __importStar(require("../../config"));
|
|
38
|
+
// Mock dependencies
|
|
39
|
+
jest.mock('../../config', () => ({
|
|
40
|
+
getConfig: jest.fn(),
|
|
41
|
+
}));
|
|
42
|
+
jest.mock('../../api/client', () => ({
|
|
43
|
+
createApi: jest.fn(),
|
|
44
|
+
}));
|
|
45
|
+
describe('Search Command', () => {
|
|
46
|
+
const mockGetConfig = config.getConfig;
|
|
47
|
+
const mockCreateApi = require('../../api/client').createApi;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
jest.clearAllMocks();
|
|
50
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
51
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
52
|
+
// Re-establish process.exit mock for each test
|
|
53
|
+
jest.spyOn(process, 'exit').mockImplementation((code) => {
|
|
54
|
+
throw new Error(`Process exit with code ${code}`);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
jest.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
describe('search success', () => {
|
|
61
|
+
it('should search rules by query', async () => {
|
|
62
|
+
mockGetConfig.mockResolvedValue({
|
|
63
|
+
server: 'http://localhost:3000',
|
|
64
|
+
token: 'test-token',
|
|
65
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
66
|
+
lastLogin: new Date().toISOString(),
|
|
67
|
+
});
|
|
68
|
+
const mockRulesApi = {
|
|
69
|
+
rulesControllerFindAll: jest.fn().mockResolvedValue({
|
|
70
|
+
data: [
|
|
71
|
+
{
|
|
72
|
+
id: 'rule-1',
|
|
73
|
+
name: 'Authentication Rule',
|
|
74
|
+
tag: 'auth',
|
|
75
|
+
type: 'rule',
|
|
76
|
+
description: 'Handles user authentication',
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
author: { username: 'user1' },
|
|
79
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
|
|
85
|
+
await (0, search_1.searchCommand)('auth', {});
|
|
86
|
+
expect(mockCreateApi).toHaveBeenCalledWith({ server: 'http://localhost:3000' });
|
|
87
|
+
expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({
|
|
88
|
+
name: 'auth',
|
|
89
|
+
});
|
|
90
|
+
expect(console.log).toHaveBeenCalledWith('🔍 Search results for "auth":');
|
|
91
|
+
});
|
|
92
|
+
it('should apply additional filters with search query', async () => {
|
|
93
|
+
mockGetConfig.mockResolvedValue({
|
|
94
|
+
server: 'http://localhost:3000',
|
|
95
|
+
token: 'test-token',
|
|
96
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
97
|
+
lastLogin: new Date().toISOString(),
|
|
98
|
+
});
|
|
99
|
+
const mockRulesApi = {
|
|
100
|
+
rulesControllerFindAll: jest.fn().mockResolvedValue({
|
|
101
|
+
data: [
|
|
102
|
+
{
|
|
103
|
+
id: 'rule-1',
|
|
104
|
+
name: 'Validation Rule',
|
|
105
|
+
tag: 'validation',
|
|
106
|
+
type: 'rule',
|
|
107
|
+
description: 'Input validation rule',
|
|
108
|
+
version: '1.0.0',
|
|
109
|
+
author: { username: 'user1' },
|
|
110
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
|
|
116
|
+
await (0, search_1.searchCommand)('validation', { type: 'rule', limit: '5' });
|
|
117
|
+
expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({
|
|
118
|
+
name: 'validation',
|
|
119
|
+
type: 'rule',
|
|
120
|
+
limit: 5,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('should handle no search results', async () => {
|
|
124
|
+
mockGetConfig.mockResolvedValue({
|
|
125
|
+
server: 'http://localhost:3000',
|
|
126
|
+
token: 'test-token',
|
|
127
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
128
|
+
lastLogin: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
const mockRulesApi = {
|
|
131
|
+
rulesControllerFindAll: jest.fn().mockResolvedValue({
|
|
132
|
+
data: [],
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
|
|
136
|
+
await (0, search_1.searchCommand)('nonexistent', {});
|
|
137
|
+
expect(console.log).toHaveBeenCalledWith('🔍 No rules or commands found matching "nonexistent".');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('search failure', () => {
|
|
141
|
+
it('should exit with error when not logged in', async () => {
|
|
142
|
+
mockGetConfig.mockResolvedValue({
|
|
143
|
+
server: 'http://localhost:3000',
|
|
144
|
+
token: undefined,
|
|
145
|
+
user: undefined,
|
|
146
|
+
});
|
|
147
|
+
await expect((0, search_1.searchCommand)('test', {})).rejects.toThrow('Process exit with code 1');
|
|
148
|
+
expect(console.error).toHaveBeenCalledWith('❌ Not logged in. Please run `kitstore login` first.');
|
|
149
|
+
});
|
|
150
|
+
it('should validate query parameter', async () => {
|
|
151
|
+
mockGetConfig.mockResolvedValue({
|
|
152
|
+
server: 'http://localhost:3000',
|
|
153
|
+
token: 'test-token',
|
|
154
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
155
|
+
lastLogin: new Date().toISOString(),
|
|
156
|
+
});
|
|
157
|
+
await expect((0, search_1.searchCommand)('', {})).rejects.toThrow('Process exit with code 1');
|
|
158
|
+
expect(console.error).toHaveBeenCalledWith('❌ Search query is required.');
|
|
159
|
+
});
|
|
160
|
+
it('should handle API errors gracefully', async () => {
|
|
161
|
+
mockGetConfig.mockResolvedValue({
|
|
162
|
+
server: 'http://localhost:3000',
|
|
163
|
+
token: 'test-token',
|
|
164
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
165
|
+
lastLogin: new Date().toISOString(),
|
|
166
|
+
});
|
|
167
|
+
const mockRulesApi = {
|
|
168
|
+
rulesControllerFindAll: jest.fn().mockRejectedValue(new Error('API Error')),
|
|
169
|
+
};
|
|
170
|
+
mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
|
|
171
|
+
await expect((0, search_1.searchCommand)('test', {})).rejects.toThrow('Process exit with code 1');
|
|
172
|
+
expect(console.error).toHaveBeenCalledWith('❌ Search failed:', expect.any(Error));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const upload_1 = require("../../commands/upload");
|
|
40
|
+
const config = __importStar(require("../../config"));
|
|
41
|
+
const axios_1 = __importDefault(require("axios"));
|
|
42
|
+
const fs = __importStar(require("fs-extra"));
|
|
43
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
44
|
+
// Mock dependencies
|
|
45
|
+
jest.mock('../../config', () => ({
|
|
46
|
+
getConfig: jest.fn(),
|
|
47
|
+
}));
|
|
48
|
+
jest.mock('axios');
|
|
49
|
+
jest.mock('fs-extra', () => ({
|
|
50
|
+
pathExists: jest.fn(),
|
|
51
|
+
stat: jest.fn(),
|
|
52
|
+
createReadStream: jest.fn(),
|
|
53
|
+
}));
|
|
54
|
+
jest.mock('form-data', () => {
|
|
55
|
+
return jest.fn().mockImplementation(function () {
|
|
56
|
+
const data = new Map();
|
|
57
|
+
return {
|
|
58
|
+
append: jest.fn((key, value) => {
|
|
59
|
+
data.set(key, value);
|
|
60
|
+
}),
|
|
61
|
+
get: jest.fn((key) => {
|
|
62
|
+
return data.get(key);
|
|
63
|
+
}),
|
|
64
|
+
getHeaders: jest.fn(() => ({
|
|
65
|
+
'Content-Type': 'multipart/form-data',
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// TC-UNIT-CLI-004 to TC-UNIT-CLI-005
|
|
71
|
+
describe('Upload Command', () => {
|
|
72
|
+
const mockGetConfig = config.getConfig;
|
|
73
|
+
const mockAxiosPost = axios_1.default.post;
|
|
74
|
+
const mockFsPathExists = fs.pathExists;
|
|
75
|
+
const mockFsStat = fs.stat;
|
|
76
|
+
const mockFsCreateReadStream = fs.createReadStream;
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
jest.clearAllMocks();
|
|
79
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
80
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
81
|
+
jest.spyOn(process, 'exit').mockImplementation((code) => {
|
|
82
|
+
throw new Error(`Process exit with code ${code}`);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
jest.restoreAllMocks();
|
|
87
|
+
});
|
|
88
|
+
// TC-UNIT-CLI-004: Upload command (success)
|
|
89
|
+
describe('upload success', () => {
|
|
90
|
+
it('should upload file successfully', async () => {
|
|
91
|
+
mockGetConfig.mockResolvedValue({
|
|
92
|
+
server: 'http://localhost:3000',
|
|
93
|
+
token: 'test-token',
|
|
94
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
95
|
+
lastLogin: new Date().toISOString(),
|
|
96
|
+
});
|
|
97
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
98
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
99
|
+
mockFsCreateReadStream.mockReturnValue({});
|
|
100
|
+
const mockFormData = {
|
|
101
|
+
append: jest.fn(),
|
|
102
|
+
getHeaders: jest.fn().mockReturnValue({ 'content-type': 'multipart/form-data' }),
|
|
103
|
+
};
|
|
104
|
+
form_data_1.default.mockImplementation(() => mockFormData);
|
|
105
|
+
mockAxiosPost.mockResolvedValue({
|
|
106
|
+
data: {
|
|
107
|
+
id: 'rule-id',
|
|
108
|
+
name: 'test.md',
|
|
109
|
+
tag: 'test',
|
|
110
|
+
type: 'rule',
|
|
111
|
+
version: '1.0.0',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
await (0, upload_1.uploadCommand)('./test.md', {
|
|
115
|
+
name: 'Test Rule',
|
|
116
|
+
tag: 'test',
|
|
117
|
+
type: 'rule',
|
|
118
|
+
description: 'Test description',
|
|
119
|
+
});
|
|
120
|
+
expect(mockFsPathExists).toHaveBeenCalledWith('./test.md');
|
|
121
|
+
expect(mockAxiosPost).toHaveBeenCalledWith('http://localhost:3000/api/rules', expect.any(Object), expect.objectContaining({
|
|
122
|
+
headers: expect.objectContaining({
|
|
123
|
+
Authorization: 'Bearer test-token',
|
|
124
|
+
}),
|
|
125
|
+
}));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// TC-UNIT-CLI-005: Upload command (file not found)
|
|
129
|
+
describe('upload failure', () => {
|
|
130
|
+
it('should handle file not found error', async () => {
|
|
131
|
+
mockGetConfig.mockResolvedValue({
|
|
132
|
+
server: 'http://localhost:3000',
|
|
133
|
+
token: 'test-token',
|
|
134
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
135
|
+
lastLogin: new Date().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
mockFsPathExists.mockResolvedValue(false);
|
|
138
|
+
await expect((0, upload_1.uploadCommand)('./nonexistent.md', {
|
|
139
|
+
name: 'Test Rule',
|
|
140
|
+
tag: 'test',
|
|
141
|
+
type: 'rule',
|
|
142
|
+
})).rejects.toThrow();
|
|
143
|
+
expect(mockAxiosPost).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
// TC-UNIT-CLI-033: Upload with file size validation
|
|
146
|
+
it('should validate file size limits', async () => {
|
|
147
|
+
mockGetConfig.mockResolvedValue({
|
|
148
|
+
server: 'http://localhost:3000',
|
|
149
|
+
token: 'test-token',
|
|
150
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
151
|
+
lastLogin: new Date().toISOString(),
|
|
152
|
+
});
|
|
153
|
+
// Mock a large file (20MB)
|
|
154
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
155
|
+
mockFsStat.mockResolvedValue({ size: 20 * 1024 * 1024 }); // 20MB
|
|
156
|
+
await expect((0, upload_1.uploadCommand)('./large.md', {
|
|
157
|
+
name: 'Large Rule',
|
|
158
|
+
tag: 'test',
|
|
159
|
+
type: 'rule',
|
|
160
|
+
})).rejects.toThrow('Process exit with code 1');
|
|
161
|
+
expect(console.error).toHaveBeenCalledWith('❌ File size exceeds limit (20.00 MB > 10 MB)');
|
|
162
|
+
expect(mockAxiosPost).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
// TC-UNIT-CLI-034: Upload with file type validation
|
|
165
|
+
it('should validate file type', async () => {
|
|
166
|
+
mockGetConfig.mockResolvedValue({
|
|
167
|
+
server: 'http://localhost:3000',
|
|
168
|
+
token: 'test-token',
|
|
169
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
170
|
+
lastLogin: new Date().toISOString(),
|
|
171
|
+
});
|
|
172
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
173
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
174
|
+
await expect((0, upload_1.uploadCommand)('./invalid.exe', {
|
|
175
|
+
name: 'Invalid Rule',
|
|
176
|
+
tag: 'test',
|
|
177
|
+
type: 'rule',
|
|
178
|
+
})).rejects.toThrow('Process exit with code 1');
|
|
179
|
+
expect(console.error).toHaveBeenCalledWith('❌ Invalid file type. Allowed types: .md, .txt, .json, .cursorrules');
|
|
180
|
+
expect(mockAxiosPost).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
// TC-UNIT-CLI-035: Upload with API failure
|
|
183
|
+
it.skip('should handle API upload failure', async () => {
|
|
184
|
+
mockGetConfig.mockResolvedValue({
|
|
185
|
+
server: 'http://localhost:3000',
|
|
186
|
+
token: 'test-token',
|
|
187
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
188
|
+
lastLogin: new Date().toISOString(),
|
|
189
|
+
});
|
|
190
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
191
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
192
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 });
|
|
193
|
+
mockAxiosPost.mockRejectedValue({
|
|
194
|
+
response: {
|
|
195
|
+
status: 413,
|
|
196
|
+
data: { error: 'File too large' },
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
await expect((0, upload_1.uploadCommand)('./test.md', {
|
|
200
|
+
name: 'Test Rule',
|
|
201
|
+
tag: 'test',
|
|
202
|
+
type: 'rule',
|
|
203
|
+
})).rejects.toThrow('Process exit with code 1');
|
|
204
|
+
expect(console.error).toHaveBeenCalledWith('❌ Upload failed: File too large');
|
|
205
|
+
});
|
|
206
|
+
// TC-UNIT-CLI-036: Upload with authentication error
|
|
207
|
+
it.skip('should handle authentication errors', async () => {
|
|
208
|
+
mockGetConfig.mockResolvedValue({
|
|
209
|
+
server: 'http://localhost:3000',
|
|
210
|
+
token: 'invalid-token',
|
|
211
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
212
|
+
lastLogin: new Date().toISOString(),
|
|
213
|
+
});
|
|
214
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
215
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
216
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 });
|
|
217
|
+
mockAxiosPost.mockRejectedValue({
|
|
218
|
+
response: {
|
|
219
|
+
status: 401,
|
|
220
|
+
data: { error: 'Invalid token' },
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
await expect((0, upload_1.uploadCommand)('./test.md', {
|
|
224
|
+
name: 'Test Rule',
|
|
225
|
+
tag: 'test',
|
|
226
|
+
type: 'rule',
|
|
227
|
+
})).rejects.toThrow('Process exit with code 1');
|
|
228
|
+
expect(console.error).toHaveBeenCalledWith('❌ Upload failed: Invalid token');
|
|
229
|
+
});
|
|
230
|
+
// TC-UNIT-CLI-037: Upload with network error
|
|
231
|
+
it.skip('should handle network errors', async () => {
|
|
232
|
+
mockGetConfig.mockResolvedValue({
|
|
233
|
+
server: 'http://localhost:3000',
|
|
234
|
+
token: 'test-token',
|
|
235
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
236
|
+
lastLogin: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
239
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
240
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 });
|
|
241
|
+
mockAxiosPost.mockRejectedValue(new Error('Network connection failed'));
|
|
242
|
+
await expect((0, upload_1.uploadCommand)('./test.md', {
|
|
243
|
+
name: 'Test Rule',
|
|
244
|
+
tag: 'test',
|
|
245
|
+
type: 'rule',
|
|
246
|
+
})).rejects.toThrow('Process exit with code 1');
|
|
247
|
+
expect(console.error).toHaveBeenCalledWith('❌ Upload failed:', 'Network connection failed');
|
|
248
|
+
});
|
|
249
|
+
// TC-UNIT-CLI-038: Upload with metadata extraction
|
|
250
|
+
it.skip('should extract and send metadata correctly', async () => {
|
|
251
|
+
mockGetConfig.mockResolvedValue({
|
|
252
|
+
server: 'http://localhost:3000',
|
|
253
|
+
token: 'test-token',
|
|
254
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
255
|
+
lastLogin: new Date().toISOString(),
|
|
256
|
+
});
|
|
257
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
258
|
+
mockFsStat.mockResolvedValue({ size: 2048 }); // Medium file
|
|
259
|
+
mockFsCreateReadStream.mockReturnValue({ size: 2048 });
|
|
260
|
+
mockAxiosPost.mockResolvedValue({
|
|
261
|
+
data: {
|
|
262
|
+
id: 'uploaded-rule-id',
|
|
263
|
+
name: 'Test Rule',
|
|
264
|
+
tag: 'test',
|
|
265
|
+
type: 'rule',
|
|
266
|
+
version: '1.0.0',
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
await (0, upload_1.uploadCommand)('./test.md', {
|
|
270
|
+
name: 'Test Rule',
|
|
271
|
+
tag: 'test',
|
|
272
|
+
type: 'rule',
|
|
273
|
+
description: 'A comprehensive test rule',
|
|
274
|
+
version: '2.0.0',
|
|
275
|
+
});
|
|
276
|
+
expect(mockAxiosPost).toHaveBeenCalledWith('http://localhost:3000/api/rules', expect.any(Object), expect.objectContaining({
|
|
277
|
+
headers: expect.objectContaining({
|
|
278
|
+
Authorization: 'Bearer test-token',
|
|
279
|
+
}),
|
|
280
|
+
}));
|
|
281
|
+
// Verify the FormData contains correct metadata
|
|
282
|
+
const formDataCall = mockAxiosPost.mock.calls[0][1];
|
|
283
|
+
expect(formDataCall.get('name')).toBe('Test Rule');
|
|
284
|
+
expect(formDataCall.get('tag')).toBe('test');
|
|
285
|
+
expect(formDataCall.get('type')).toBe('rule');
|
|
286
|
+
expect(formDataCall.get('description')).toBe('A comprehensive test rule');
|
|
287
|
+
expect(formDataCall.get('version')).toBe('2.0.0');
|
|
288
|
+
});
|
|
289
|
+
// TC-UNIT-CLI-039: Upload with default version
|
|
290
|
+
it.skip('should use default version when not specified', async () => {
|
|
291
|
+
mockGetConfig.mockResolvedValue({
|
|
292
|
+
server: 'http://localhost:3000',
|
|
293
|
+
token: 'test-token',
|
|
294
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
295
|
+
lastLogin: new Date().toISOString(),
|
|
296
|
+
});
|
|
297
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
298
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
299
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 });
|
|
300
|
+
mockAxiosPost.mockResolvedValue({
|
|
301
|
+
data: {
|
|
302
|
+
id: 'uploaded-rule-id',
|
|
303
|
+
name: 'Test Rule',
|
|
304
|
+
version: '1.0.0',
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
await (0, upload_1.uploadCommand)('./test.md', {
|
|
308
|
+
name: 'Test Rule',
|
|
309
|
+
tag: 'test',
|
|
310
|
+
type: 'rule',
|
|
311
|
+
});
|
|
312
|
+
const formDataCall = mockAxiosPost.mock.calls[0][1];
|
|
313
|
+
expect(formDataCall.get('version')).toBe('1.0.0'); // Default version
|
|
314
|
+
});
|
|
315
|
+
// TC-UNIT-CLI-040: Upload with missing required parameters
|
|
316
|
+
it.skip('should validate required parameters', async () => {
|
|
317
|
+
mockGetConfig.mockResolvedValue({
|
|
318
|
+
server: 'http://localhost:3000',
|
|
319
|
+
token: 'test-token',
|
|
320
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
321
|
+
lastLogin: new Date().toISOString(),
|
|
322
|
+
});
|
|
323
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
324
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
325
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 });
|
|
326
|
+
// Missing tag
|
|
327
|
+
await expect((0, upload_1.uploadCommand)('./test.md', {
|
|
328
|
+
name: 'Test Rule',
|
|
329
|
+
type: 'rule',
|
|
330
|
+
})).rejects.toThrow('Process exit with code 1');
|
|
331
|
+
expect(console.error).toHaveBeenCalledWith('❌ Missing required parameters: tag, type');
|
|
332
|
+
});
|
|
333
|
+
// TC-UNIT-CLI-041: Upload command success with all parameters
|
|
334
|
+
it.skip('should handle successful upload with all parameters', async () => {
|
|
335
|
+
mockGetConfig.mockResolvedValue({
|
|
336
|
+
server: 'http://localhost:3000',
|
|
337
|
+
token: 'test-token',
|
|
338
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
339
|
+
lastLogin: new Date().toISOString(),
|
|
340
|
+
});
|
|
341
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
342
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
343
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 });
|
|
344
|
+
mockAxiosPost.mockResolvedValue({
|
|
345
|
+
data: {
|
|
346
|
+
id: 'uploaded-rule-id',
|
|
347
|
+
name: 'Comprehensive Rule',
|
|
348
|
+
tag: 'comprehensive',
|
|
349
|
+
type: 'rule',
|
|
350
|
+
version: '3.0.0',
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
await (0, upload_1.uploadCommand)('./comprehensive.md', {
|
|
354
|
+
name: 'Comprehensive Rule',
|
|
355
|
+
tag: 'comprehensive',
|
|
356
|
+
type: 'rule',
|
|
357
|
+
description: 'A comprehensive test rule',
|
|
358
|
+
version: '3.0.0',
|
|
359
|
+
});
|
|
360
|
+
expect(console.log).toHaveBeenCalledWith('✅ Successfully uploaded rule: Comprehensive Rule');
|
|
361
|
+
expect(console.log).toHaveBeenCalledWith('📋 Rule ID: uploaded-rule-id');
|
|
362
|
+
expect(console.log).toHaveBeenCalledWith('🏷️ Tag: comprehensive');
|
|
363
|
+
expect(console.log).toHaveBeenCalledWith('📄 Type: rule');
|
|
364
|
+
expect(console.log).toHaveBeenCalledWith('🔢 Version: 3.0.0');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|