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.
Files changed (71) hide show
  1. package/.env.test +4 -0
  2. package/.eslintrc.js +29 -0
  3. package/dist/__tests__/commands/init.test.js +76 -0
  4. package/dist/__tests__/commands/install.test.js +422 -0
  5. package/dist/__tests__/commands/list.test.js +173 -0
  6. package/dist/__tests__/commands/login.test.js +281 -0
  7. package/dist/__tests__/commands/rule-check.test.js +72 -0
  8. package/dist/__tests__/commands/search.test.js +175 -0
  9. package/dist/__tests__/commands/upload.test.js +367 -0
  10. package/dist/__tests__/config.test.js +179 -0
  11. package/dist/__tests__/setup.js +8 -0
  12. package/dist/api/client.js +18 -0
  13. package/dist/api/generated/api.js +912 -0
  14. package/dist/api/generated/base.js +48 -0
  15. package/dist/api/generated/common.js +108 -0
  16. package/dist/api/generated/configuration.js +48 -0
  17. package/dist/api/generated/index.js +31 -0
  18. package/dist/commands/init.js +79 -0
  19. package/dist/commands/install.js +150 -0
  20. package/dist/commands/list.js +70 -0
  21. package/dist/commands/login.js +64 -0
  22. package/dist/commands/rule-check.js +81 -0
  23. package/dist/commands/search.js +59 -0
  24. package/dist/commands/upload.js +138 -0
  25. package/dist/config.js +84 -0
  26. package/dist/index.js +71 -0
  27. package/e2e/install.e2e.test.ts +237 -0
  28. package/e2e/integration.e2e.test.ts +346 -0
  29. package/e2e/login.e2e.test.ts +188 -0
  30. package/jest.config.js +24 -0
  31. package/openapitools.json +7 -0
  32. package/package.json +41 -0
  33. package/src/__tests__/commands/init.test.ts +52 -0
  34. package/src/__tests__/commands/install.test.ts +449 -0
  35. package/src/__tests__/commands/list.test.ts +164 -0
  36. package/src/__tests__/commands/login.test.ts +293 -0
  37. package/src/__tests__/commands/rule-check.test.ts +52 -0
  38. package/src/__tests__/commands/search.test.ts +168 -0
  39. package/src/__tests__/commands/upload.test.ts +404 -0
  40. package/src/__tests__/config.test.ts +181 -0
  41. package/src/__tests__/setup.ts +11 -0
  42. package/src/api/client.ts +20 -0
  43. package/src/api/generated/.openapi-generator/FILES +17 -0
  44. package/src/api/generated/.openapi-generator/VERSION +1 -0
  45. package/src/api/generated/.openapi-generator-ignore +23 -0
  46. package/src/api/generated/api.ts +1171 -0
  47. package/src/api/generated/base.ts +62 -0
  48. package/src/api/generated/common.ts +113 -0
  49. package/src/api/generated/configuration.ts +121 -0
  50. package/src/api/generated/docs/AuthApi.md +158 -0
  51. package/src/api/generated/docs/AuthResponseDto.md +22 -0
  52. package/src/api/generated/docs/AuthUserDto.md +24 -0
  53. package/src/api/generated/docs/HealthApi.md +183 -0
  54. package/src/api/generated/docs/LoginDto.md +22 -0
  55. package/src/api/generated/docs/RegisterDto.md +24 -0
  56. package/src/api/generated/docs/RuleAuthorDto.md +22 -0
  57. package/src/api/generated/docs/RuleResponseDto.md +36 -0
  58. package/src/api/generated/docs/RulesApi.md +289 -0
  59. package/src/api/generated/git_push.sh +57 -0
  60. package/src/api/generated/index.ts +18 -0
  61. package/src/commands/init.ts +46 -0
  62. package/src/commands/install.ts +129 -0
  63. package/src/commands/list.ts +71 -0
  64. package/src/commands/login.ts +65 -0
  65. package/src/commands/rule-check.ts +49 -0
  66. package/src/commands/search.ts +66 -0
  67. package/src/commands/upload.ts +117 -0
  68. package/src/config.ts +66 -0
  69. package/src/index.ts +79 -0
  70. package/test-cli-config.js +118 -0
  71. 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
+ });