cligr 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.
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Integration tests for ConfigLoader
3
+ *
4
+ * These tests verify the config loading functionality including:
5
+ * - Loading from different file locations
6
+ * - YAML parsing
7
+ * - Validation
8
+ * - Group retrieval
9
+ */
10
+
11
+ import { describe, it, before, after, mock } from 'node:test';
12
+ import assert from 'node:assert';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+ import { ConfigLoader, ConfigError } from '../../src/config/loader.js';
17
+
18
+ describe('ConfigLoader Integration Tests', () => {
19
+ let testConfigDir: string;
20
+ let testConfigPath: string;
21
+ let originalHomeDir: string;
22
+
23
+ before(() => {
24
+ // Create a temporary directory for test configs
25
+ testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cligr-test-'));
26
+ testConfigPath = path.join(testConfigDir, '.cligr.yml');
27
+
28
+ // Mock os.homedir to return our test directory
29
+ originalHomeDir = os.homedir();
30
+ mock.method(os, 'homedir', () => testConfigDir);
31
+ });
32
+
33
+ after(() => {
34
+ // Clean up test directory
35
+ if (fs.existsSync(testConfigDir)) {
36
+ fs.rmSync(testConfigDir, { recursive: true, force: true });
37
+ }
38
+ // Restore original os.homedir
39
+ mock.method(os, 'homedir', () => originalHomeDir);
40
+ });
41
+
42
+ describe('load()', () => {
43
+ it('should load a valid config file from home directory', () => {
44
+ const configContent = `
45
+ tools:
46
+ docker:
47
+ cmd: docker run -it --rm
48
+ node:
49
+ cmd: node
50
+
51
+ groups:
52
+ test1:
53
+ tool: docker
54
+ restart: yes
55
+ items:
56
+ - alpine,sh
57
+ - nginx,nginx,-p,80:80
58
+
59
+ test2:
60
+ tool: node
61
+ restart: no
62
+ items:
63
+ - server.js
64
+ - worker.js
65
+ `;
66
+
67
+ fs.writeFileSync(testConfigPath, configContent);
68
+
69
+ const loader = new ConfigLoader();
70
+ const config = loader.load();
71
+
72
+ assert.ok(config.groups);
73
+ assert.ok(config.tools);
74
+ assert.strictEqual(Object.keys(config.groups).length, 2);
75
+ assert.strictEqual(config.groups.test1.tool, 'docker');
76
+ assert.strictEqual(config.groups.test1.restart, 'yes');
77
+ assert.strictEqual(config.groups.test1.items.length, 2);
78
+ assert.strictEqual(config.groups.test2.tool, 'node');
79
+ assert.strictEqual(config.groups.test2.items.length, 2);
80
+ });
81
+
82
+ it('should throw ConfigError when config file does not exist', () => {
83
+ // Remove config file if it exists
84
+ if (fs.existsSync(testConfigPath)) {
85
+ fs.unlinkSync(testConfigPath);
86
+ }
87
+
88
+ const loader = new ConfigLoader();
89
+
90
+ assert.throws(
91
+ () => loader.load(),
92
+ (err: Error) => {
93
+ assert.ok(err instanceof ConfigError);
94
+ assert.ok(err.message.includes('Config file not found'));
95
+ return true;
96
+ }
97
+ );
98
+ });
99
+
100
+ it('should throw ConfigError for invalid YAML', () => {
101
+ fs.writeFileSync(testConfigPath, 'invalid: yaml: content: [unclosed');
102
+
103
+ const loader = new ConfigLoader();
104
+
105
+ assert.throws(
106
+ () => loader.load(),
107
+ (err: Error) => {
108
+ assert.ok(err instanceof ConfigError);
109
+ assert.ok(err.message.includes('Invalid YAML'));
110
+ return true;
111
+ }
112
+ );
113
+ });
114
+
115
+ it('should throw ConfigError when config is not an object', () => {
116
+ fs.writeFileSync(testConfigPath, 'just a string');
117
+
118
+ const loader = new ConfigLoader();
119
+
120
+ assert.throws(
121
+ () => loader.load(),
122
+ (err: Error) => {
123
+ assert.ok(err instanceof ConfigError);
124
+ assert.ok(err.message.includes('Config must be an object'));
125
+ return true;
126
+ }
127
+ );
128
+ });
129
+
130
+ it('should throw ConfigError when groups field is missing', () => {
131
+ fs.writeFileSync(testConfigPath, 'tools:\n docker:\n cmd: docker');
132
+
133
+ const loader = new ConfigLoader();
134
+
135
+ assert.throws(
136
+ () => loader.load(),
137
+ (err: Error) => {
138
+ assert.ok(err instanceof ConfigError);
139
+ assert.ok(err.message.includes('groups'));
140
+ return true;
141
+ }
142
+ );
143
+ });
144
+
145
+ it('should load config without tools section', () => {
146
+ const configContent = `
147
+ groups:
148
+ simple:
149
+ tool: echo
150
+ restart: no
151
+ items:
152
+ - hello
153
+ - world
154
+ `;
155
+
156
+ fs.writeFileSync(testConfigPath, configContent);
157
+
158
+ const loader = new ConfigLoader();
159
+ const config = loader.load();
160
+
161
+ assert.ok(config.groups);
162
+ assert.strictEqual(Object.keys(config.groups).length, 1);
163
+ assert.strictEqual(config.groups.simple.tool, 'echo');
164
+ });
165
+ });
166
+
167
+ describe('getGroup()', () => {
168
+ before(() => {
169
+ const configContent = `
170
+ tools:
171
+ docker:
172
+ cmd: docker run -it --rm
173
+
174
+ groups:
175
+ web:
176
+ tool: docker
177
+ restart: unless-stopped
178
+ items:
179
+ - nginx,nginx,-p,80:80
180
+ - redis,redis
181
+
182
+ api:
183
+ tool: node
184
+ restart: yes
185
+ items:
186
+ - server.js,3000
187
+ - worker.js
188
+ `;
189
+
190
+ fs.writeFileSync(testConfigPath, configContent);
191
+ });
192
+
193
+ it('should retrieve an existing group with tool', () => {
194
+ const loader = new ConfigLoader();
195
+ const result = loader.getGroup('web');
196
+
197
+ assert.strictEqual(result.config.tool, 'docker');
198
+ assert.strictEqual(result.config.restart, 'unless-stopped');
199
+ assert.strictEqual(result.tool, 'docker');
200
+ assert.strictEqual(result.toolTemplate, 'docker run -it --rm');
201
+ assert.strictEqual(result.config.items.length, 2);
202
+ });
203
+
204
+ it('should retrieve an existing group without registered tool', () => {
205
+ const loader = new ConfigLoader();
206
+ const result = loader.getGroup('api');
207
+
208
+ assert.strictEqual(result.config.tool, 'node');
209
+ assert.strictEqual(result.config.restart, 'yes');
210
+ assert.strictEqual(result.tool, null); // No registered tool
211
+ assert.strictEqual(result.toolTemplate, null);
212
+ assert.strictEqual(result.config.items.length, 2);
213
+ });
214
+
215
+ it('should throw ConfigError for unknown group', () => {
216
+ const loader = new ConfigLoader();
217
+
218
+ assert.throws(
219
+ () => loader.getGroup('unknown'),
220
+ (err: Error) => {
221
+ assert.ok(err instanceof ConfigError);
222
+ assert.ok(err.message.includes('Unknown group'));
223
+ assert.ok(err.message.includes('unknown'));
224
+ return true;
225
+ }
226
+ );
227
+ });
228
+ });
229
+
230
+ describe('listGroups()', () => {
231
+ it('should return all group names', () => {
232
+ const configContent = `
233
+ groups:
234
+ group1:
235
+ tool: echo
236
+ restart: no
237
+ items:
238
+ - test1
239
+
240
+ group2:
241
+ tool: echo
242
+ restart: no
243
+ items:
244
+ - test2
245
+
246
+ group3:
247
+ tool: echo
248
+ restart: no
249
+ items:
250
+ - test3
251
+ `;
252
+
253
+ fs.writeFileSync(testConfigPath, configContent);
254
+
255
+ const loader = new ConfigLoader();
256
+ const groups = loader.listGroups();
257
+
258
+ assert.strictEqual(groups.length, 3);
259
+ assert.ok(groups.includes('group1'));
260
+ assert.ok(groups.includes('group2'));
261
+ assert.ok(groups.includes('group3'));
262
+ });
263
+
264
+ it('should return empty array when no groups exist', () => {
265
+ const configContent = `
266
+ groups: {}
267
+ `;
268
+
269
+ fs.writeFileSync(testConfigPath, configContent);
270
+
271
+ const loader = new ConfigLoader();
272
+ const groups = loader.listGroups();
273
+
274
+ assert.strictEqual(groups.length, 0);
275
+ });
276
+ });
277
+
278
+ describe('Constructor with explicit path', () => {
279
+ it('should load config from explicit path', () => {
280
+ const customConfigPath = path.join(testConfigDir, 'custom-config.yml');
281
+ const configContent = `
282
+ groups:
283
+ custom:
284
+ tool: echo
285
+ restart: no
286
+ items:
287
+ - test
288
+ `;
289
+
290
+ fs.writeFileSync(customConfigPath, configContent);
291
+
292
+ const loader = new ConfigLoader(customConfigPath);
293
+ const config = loader.load();
294
+
295
+ assert.ok(config.groups);
296
+ assert.strictEqual(config.groups.custom.tool, 'echo');
297
+ });
298
+
299
+ it('should throw error when explicit path does not exist', () => {
300
+ const nonExistentPath = path.join(testConfigDir, 'does-not-exist.yml');
301
+
302
+ const loader = new ConfigLoader(nonExistentPath);
303
+
304
+ assert.throws(
305
+ () => loader.load(),
306
+ (err: Error) => {
307
+ assert.ok(err instanceof ConfigError);
308
+ return true;
309
+ }
310
+ );
311
+ });
312
+ });
313
+
314
+ describe('Config precedence', () => {
315
+ it('should prefer home directory config over current directory', () => {
316
+ const homeConfigPath = path.join(testConfigDir, '.cligr.yml');
317
+ const currentConfigPath = path.join(process.cwd(), '.cligr.yml');
318
+
319
+ const homeContent = `
320
+ groups:
321
+ home-group:
322
+ tool: echo
323
+ restart: no
324
+ items:
325
+ - from-home
326
+ `;
327
+
328
+ const currentContent = `
329
+ groups:
330
+ current-group:
331
+ tool: echo
332
+ restart: no
333
+ items:
334
+ - from-current
335
+ `;
336
+
337
+ // Write home config (mocked to testConfigDir)
338
+ fs.writeFileSync(homeConfigPath, homeContent);
339
+
340
+ // Write current directory config
341
+ fs.writeFileSync(currentConfigPath, currentContent);
342
+
343
+ const loader = new ConfigLoader();
344
+ const groups = loader.listGroups();
345
+
346
+ // Should load from home directory
347
+ assert.strictEqual(groups.length, 1);
348
+ assert.strictEqual(groups[0], 'home-group');
349
+
350
+ // Clean up current directory config
351
+ fs.unlinkSync(currentConfigPath);
352
+ });
353
+ });
354
+
355
+ describe('Edge cases', () => {
356
+ it('should handle empty items array', () => {
357
+ const configContent = `
358
+ groups:
359
+ empty:
360
+ tool: echo
361
+ restart: no
362
+ items: []
363
+ `;
364
+
365
+ fs.writeFileSync(testConfigPath, configContent);
366
+
367
+ const loader = new ConfigLoader();
368
+ const config = loader.load();
369
+
370
+ assert.strictEqual(config.groups.empty.items.length, 0);
371
+ });
372
+
373
+ it('should handle special characters in item strings', () => {
374
+ const configContent = `
375
+ groups:
376
+ special:
377
+ tool: echo
378
+ restart: no
379
+ items:
380
+ - "hello, world"
381
+ - "test,with,commas"
382
+ - "spaces test"
383
+ `;
384
+
385
+ fs.writeFileSync(testConfigPath, configContent);
386
+
387
+ const loader = new ConfigLoader();
388
+ const result = loader.getGroup('special');
389
+
390
+ assert.strictEqual(result.config.items.length, 3);
391
+ assert.strictEqual(result.config.items[0], 'hello, world');
392
+ assert.strictEqual(result.config.items[1], 'test,with,commas');
393
+ });
394
+
395
+ it('should handle all restart policy values', () => {
396
+ const configContent = `
397
+ groups:
398
+ restart-yes:
399
+ tool: echo
400
+ restart: yes
401
+ items:
402
+ - test
403
+
404
+ restart-no:
405
+ tool: echo
406
+ restart: no
407
+ items:
408
+ - test
409
+
410
+ restart-unless-stopped:
411
+ tool: echo
412
+ restart: unless-stopped
413
+ items:
414
+ - test
415
+ `;
416
+
417
+ fs.writeFileSync(testConfigPath, configContent);
418
+
419
+ const loader = new ConfigLoader();
420
+
421
+ assert.strictEqual(loader.getGroup('restart-yes').config.restart, 'yes');
422
+ assert.strictEqual(loader.getGroup('restart-no').config.restart, 'no');
423
+ assert.strictEqual(loader.getGroup('restart-unless-stopped').config.restart, 'unless-stopped');
424
+ });
425
+ });
426
+ });