@arela/uploader 1.0.3 → 1.0.5

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 (44) hide show
  1. package/.env.local +316 -0
  2. package/coverage/IdentifyCommand.js.html +1462 -0
  3. package/coverage/PropagateCommand.js.html +1507 -0
  4. package/coverage/PushCommand.js.html +1504 -0
  5. package/coverage/ScanCommand.js.html +1654 -0
  6. package/coverage/UploadCommand.js.html +1846 -0
  7. package/coverage/WatchCommand.js.html +4111 -0
  8. package/coverage/base.css +224 -0
  9. package/coverage/block-navigation.js +87 -0
  10. package/coverage/favicon.png +0 -0
  11. package/coverage/index.html +191 -0
  12. package/coverage/lcov-report/IdentifyCommand.js.html +1462 -0
  13. package/coverage/lcov-report/PropagateCommand.js.html +1507 -0
  14. package/coverage/lcov-report/PushCommand.js.html +1504 -0
  15. package/coverage/lcov-report/ScanCommand.js.html +1654 -0
  16. package/coverage/lcov-report/UploadCommand.js.html +1846 -0
  17. package/coverage/lcov-report/WatchCommand.js.html +4111 -0
  18. package/coverage/lcov-report/base.css +224 -0
  19. package/coverage/lcov-report/block-navigation.js +87 -0
  20. package/coverage/lcov-report/favicon.png +0 -0
  21. package/coverage/lcov-report/index.html +191 -0
  22. package/coverage/lcov-report/prettify.css +1 -0
  23. package/coverage/lcov-report/prettify.js +2 -0
  24. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  25. package/coverage/lcov-report/sorter.js +210 -0
  26. package/coverage/lcov.info +1937 -0
  27. package/coverage/prettify.css +1 -0
  28. package/coverage/prettify.js +2 -0
  29. package/coverage/sort-arrow-sprite.png +0 -0
  30. package/coverage/sorter.js +210 -0
  31. package/docs/CROSS_PLATFORM_PATH_HANDLING.md +597 -0
  32. package/package.json +28 -2
  33. package/src/commands/IdentifyCommand.js +1 -28
  34. package/src/commands/PropagateCommand.js +1 -1
  35. package/src/commands/PushCommand.js +1 -1
  36. package/src/commands/ScanCommand.js +27 -20
  37. package/src/config/config.js +27 -48
  38. package/src/services/ScanApiService.js +4 -5
  39. package/src/utils/PathNormalizer.js +272 -0
  40. package/tests/commands/IdentifyCommand.test.js +570 -0
  41. package/tests/commands/PropagateCommand.test.js +568 -0
  42. package/tests/commands/PushCommand.test.js +754 -0
  43. package/tests/commands/ScanCommand.test.js +382 -0
  44. package/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
@@ -0,0 +1,754 @@
1
+ /**
2
+ * Unit tests for PushCommand
3
+ * Tests the arela push command functionality
4
+ */
5
+ import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
6
+
7
+ // Mock dependencies before importing PushCommand
8
+ const mockLogger = {
9
+ info: jest.fn(),
10
+ success: jest.fn(),
11
+ error: jest.fn(),
12
+ warn: jest.fn(),
13
+ debug: jest.fn(),
14
+ };
15
+
16
+ const mockProgressBar = {
17
+ start: jest.fn(),
18
+ update: jest.fn(),
19
+ stop: jest.fn(),
20
+ };
21
+
22
+ const mockCliProgress = {
23
+ SingleBar: jest.fn(() => mockProgressBar),
24
+ Presets: {
25
+ shades_classic: {},
26
+ },
27
+ };
28
+
29
+ const mockScanApiService = {
30
+ getInstanceTables: jest.fn(),
31
+ getPushStats: jest.fn(),
32
+ fetchFilesForPush: jest.fn(),
33
+ batchUpdateUpload: jest.fn(),
34
+ };
35
+
36
+ const mockAppConfig = {
37
+ validateScanConfig: jest.fn(),
38
+ getScanConfig: jest.fn(),
39
+ getPushConfig: jest.fn(),
40
+ getApiConfig: jest.fn(),
41
+ setApiTarget: jest.fn(),
42
+ packageVersion: '1.0.3',
43
+ };
44
+
45
+ const mockFs = {
46
+ existsSync: jest.fn(),
47
+ statSync: jest.fn(),
48
+ createReadStream: jest.fn(),
49
+ };
50
+
51
+ const mockFetch = jest.fn();
52
+
53
+ const mockFormData = jest.fn(() => ({
54
+ append: jest.fn(),
55
+ getHeaders: jest.fn(() => ({})),
56
+ }));
57
+
58
+ // Store original console methods
59
+ const originalConsoleLog = console.log;
60
+ const originalConsoleError = console.error;
61
+ const originalProcessExit = process.exit;
62
+
63
+ jest.unstable_mockModule('cli-progress', () => ({
64
+ default: mockCliProgress,
65
+ SingleBar: mockCliProgress.SingleBar,
66
+ Presets: mockCliProgress.Presets,
67
+ }));
68
+
69
+ jest.unstable_mockModule('fs', () => ({
70
+ default: mockFs,
71
+ existsSync: mockFs.existsSync,
72
+ statSync: mockFs.statSync,
73
+ createReadStream: mockFs.createReadStream,
74
+ }));
75
+
76
+ jest.unstable_mockModule('node-fetch', () => ({
77
+ default: mockFetch,
78
+ }));
79
+
80
+ jest.unstable_mockModule('form-data', () => ({
81
+ default: mockFormData,
82
+ }));
83
+
84
+ jest.unstable_mockModule('../../src/services/LoggingService.js', () => ({
85
+ default: mockLogger,
86
+ }));
87
+
88
+ jest.unstable_mockModule('../../src/services/ScanApiService.js', () => ({
89
+ default: jest.fn(() => mockScanApiService),
90
+ }));
91
+
92
+ jest.unstable_mockModule('../../src/config/config.js', () => ({
93
+ default: mockAppConfig,
94
+ }));
95
+
96
+ // Import the class after mocking
97
+ const { PushCommand } = await import('../../src/commands/PushCommand.js');
98
+
99
+ describe('PushCommand', () => {
100
+ let pushCommand;
101
+ let mockConsoleLog;
102
+ let mockConsoleError;
103
+ let mockProcessExit;
104
+
105
+ beforeEach(() => {
106
+ jest.clearAllMocks();
107
+
108
+ // Mock console methods
109
+ mockConsoleLog = jest.fn();
110
+ mockConsoleError = jest.fn();
111
+ mockProcessExit = jest.fn();
112
+ console.log = mockConsoleLog;
113
+ console.error = mockConsoleError;
114
+ process.exit = mockProcessExit;
115
+
116
+ // Default mock implementations
117
+ mockAppConfig.getScanConfig.mockReturnValue({
118
+ companySlug: 'test-company',
119
+ serverId: 'test-server',
120
+ basePathFull: '/test/path',
121
+ tableName: 'scan_test_table',
122
+ });
123
+
124
+ mockAppConfig.getPushConfig.mockReturnValue({
125
+ bucket: 'test-bucket',
126
+ rfcs: [],
127
+ years: [],
128
+ });
129
+
130
+ mockAppConfig.getApiConfig.mockReturnValue({
131
+ baseUrl: 'https://api.test.com',
132
+ token: 'test-token',
133
+ });
134
+
135
+ mockAppConfig.validateScanConfig.mockReturnValue(undefined);
136
+
137
+ mockScanApiService.getInstanceTables.mockResolvedValue([
138
+ { tableName: 'scan_test_table' },
139
+ ]);
140
+
141
+ mockScanApiService.getPushStats.mockResolvedValue({
142
+ totalWithArelaPath: 100,
143
+ uploaded: 50,
144
+ pending: 50,
145
+ errors: 0,
146
+ maxAttemptsReached: 0,
147
+ byRfc: [],
148
+ });
149
+
150
+ mockScanApiService.fetchFilesForPush.mockResolvedValue([]);
151
+ mockScanApiService.batchUpdateUpload.mockResolvedValue({ updated: 0 });
152
+
153
+ mockFs.existsSync.mockReturnValue(true);
154
+ mockFs.statSync.mockReturnValue({ isFile: () => true, size: 1000 });
155
+ mockFs.createReadStream.mockReturnValue({});
156
+
157
+ mockFetch.mockResolvedValue({
158
+ ok: true,
159
+ json: () => Promise.resolve({ uploaded: [{ id: 'file-id' }] }),
160
+ });
161
+
162
+ pushCommand = new PushCommand();
163
+ });
164
+
165
+ afterEach(() => {
166
+ // Restore console methods
167
+ console.log = originalConsoleLog;
168
+ console.error = originalConsoleError;
169
+ process.exit = originalProcessExit;
170
+ jest.restoreAllMocks();
171
+ });
172
+
173
+ describe('constructor', () => {
174
+ it('should create a new PushCommand instance', () => {
175
+ expect(pushCommand).toBeInstanceOf(PushCommand);
176
+ expect(pushCommand.scanApiService).toBeDefined();
177
+ });
178
+ });
179
+
180
+ describe('execute', () => {
181
+ const defaultOptions = {
182
+ batchSize: '100',
183
+ uploadBatchSize: '10',
184
+ };
185
+
186
+ it('should validate configuration', async () => {
187
+ await pushCommand.execute(defaultOptions);
188
+
189
+ expect(mockAppConfig.validateScanConfig).toHaveBeenCalled();
190
+ });
191
+
192
+ it('should exit on configuration errors', async () => {
193
+ mockAppConfig.validateScanConfig.mockImplementation(() => {
194
+ throw new Error('Missing required config');
195
+ });
196
+
197
+ await pushCommand.execute(defaultOptions);
198
+
199
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
200
+ });
201
+
202
+ it('should fetch instance tables', async () => {
203
+ await pushCommand.execute(defaultOptions);
204
+
205
+ expect(mockScanApiService.getInstanceTables).toHaveBeenCalledWith(
206
+ 'test-company',
207
+ 'test-server',
208
+ '/test/path'
209
+ );
210
+ });
211
+
212
+ it('should exit when no tables found', async () => {
213
+ mockScanApiService.getInstanceTables.mockResolvedValue([]);
214
+
215
+ await pushCommand.execute(defaultOptions);
216
+
217
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
218
+ expect(mockConsoleError).toHaveBeenCalledWith(
219
+ expect.stringContaining('No tables found')
220
+ );
221
+ });
222
+
223
+ it('should set API target when specified', async () => {
224
+ await pushCommand.execute({ ...defaultOptions, api: 'agencia' });
225
+
226
+ expect(mockAppConfig.setApiTarget).toHaveBeenCalledWith('agencia');
227
+ });
228
+
229
+ it('should use different scan and push API targets', async () => {
230
+ await pushCommand.execute({
231
+ ...defaultOptions,
232
+ scanApi: 'agencia',
233
+ pushApi: 'cliente',
234
+ });
235
+
236
+ expect(mockAppConfig.setApiTarget).toHaveBeenCalledWith('agencia');
237
+ expect(mockAppConfig.getApiConfig).toHaveBeenCalledWith('cliente');
238
+ });
239
+
240
+ it('should apply RFC filters', async () => {
241
+ await pushCommand.execute({
242
+ ...defaultOptions,
243
+ rfcs: ['RFC123', 'RFC456'],
244
+ });
245
+
246
+ expect(mockConsoleLog).toHaveBeenCalledWith(
247
+ expect.stringContaining('RFC Filter: RFC123, RFC456')
248
+ );
249
+ });
250
+
251
+ it('should apply year filters', async () => {
252
+ await pushCommand.execute({
253
+ ...defaultOptions,
254
+ years: ['2023', '2024'],
255
+ });
256
+
257
+ expect(mockConsoleLog).toHaveBeenCalledWith(
258
+ expect.stringContaining('Year Filter: 2023, 2024')
259
+ );
260
+ });
261
+
262
+ it('should skip table when no pending files', async () => {
263
+ mockScanApiService.getPushStats.mockResolvedValue({
264
+ totalWithArelaPath: 100,
265
+ uploaded: 100,
266
+ pending: 0,
267
+ errors: 0,
268
+ maxAttemptsReached: 0,
269
+ });
270
+
271
+ await pushCommand.execute(defaultOptions);
272
+
273
+ expect(mockConsoleLog).toHaveBeenCalledWith(
274
+ expect.stringContaining('No files pending upload')
275
+ );
276
+ });
277
+ });
278
+
279
+ describe('file processing', () => {
280
+ const defaultOptions = {
281
+ batchSize: '100',
282
+ uploadBatchSize: '10',
283
+ };
284
+
285
+ it('should fetch files for push', async () => {
286
+ mockScanApiService.getPushStats.mockResolvedValue({
287
+ totalWithArelaPath: 10,
288
+ uploaded: 0,
289
+ pending: 10,
290
+ errors: 0,
291
+ });
292
+
293
+ mockScanApiService.fetchFilesForPush.mockResolvedValue([
294
+ {
295
+ id: 1,
296
+ absolute_path: '/test/file.pdf',
297
+ arela_path: 'RFC/2023/3429/07/12345/',
298
+ file_name: 'file.pdf',
299
+ rfc: 'RFC123',
300
+ detected_pedimento_year: 2023,
301
+ },
302
+ ]);
303
+
304
+ await pushCommand.execute(defaultOptions);
305
+
306
+ expect(mockScanApiService.fetchFilesForPush).toHaveBeenCalled();
307
+ });
308
+
309
+ it('should upload files and update database', async () => {
310
+ mockScanApiService.getPushStats.mockResolvedValue({
311
+ totalWithArelaPath: 1,
312
+ uploaded: 0,
313
+ pending: 1,
314
+ errors: 0,
315
+ });
316
+
317
+ mockScanApiService.fetchFilesForPush
318
+ .mockResolvedValueOnce([
319
+ {
320
+ id: 1,
321
+ absolute_path: '/test/file.pdf',
322
+ arela_path: 'RFC/2023/3429/07/12345/',
323
+ file_name: 'file.pdf',
324
+ rfc: 'RFC123',
325
+ detected_pedimento_year: 2023,
326
+ relative_path: 'file.pdf',
327
+ },
328
+ ])
329
+ .mockResolvedValueOnce([]);
330
+
331
+ mockFetch.mockResolvedValue({
332
+ ok: true,
333
+ json: () => Promise.resolve({ uploaded: [{ id: 'storage-id' }] }),
334
+ });
335
+
336
+ await pushCommand.execute(defaultOptions);
337
+
338
+ expect(mockScanApiService.batchUpdateUpload).toHaveBeenCalled();
339
+ });
340
+
341
+ it('should handle file not found errors', async () => {
342
+ mockScanApiService.getPushStats.mockResolvedValue({
343
+ totalWithArelaPath: 1,
344
+ uploaded: 0,
345
+ pending: 1,
346
+ errors: 0,
347
+ });
348
+
349
+ mockScanApiService.fetchFilesForPush
350
+ .mockResolvedValueOnce([
351
+ {
352
+ id: 1,
353
+ absolute_path: '/test/missing.pdf',
354
+ arela_path: 'RFC/2023/3429/07/12345/',
355
+ file_name: 'missing.pdf',
356
+ rfc: 'RFC123',
357
+ },
358
+ ])
359
+ .mockResolvedValueOnce([]);
360
+
361
+ mockFs.existsSync.mockReturnValue(false);
362
+
363
+ // Should complete without throwing (errors are tracked internally)
364
+ await expect(pushCommand.execute(defaultOptions)).resolves.not.toThrow();
365
+ });
366
+
367
+ it('should handle non-file paths', async () => {
368
+ mockScanApiService.getPushStats.mockResolvedValue({
369
+ totalWithArelaPath: 1,
370
+ uploaded: 0,
371
+ pending: 1,
372
+ errors: 0,
373
+ });
374
+
375
+ mockScanApiService.fetchFilesForPush
376
+ .mockResolvedValueOnce([
377
+ {
378
+ id: 1,
379
+ absolute_path: '/test/directory',
380
+ arela_path: 'RFC/2023/3429/07/12345/',
381
+ file_name: 'directory',
382
+ },
383
+ ])
384
+ .mockResolvedValueOnce([]);
385
+
386
+ mockFs.existsSync.mockReturnValue(true);
387
+ mockFs.statSync.mockReturnValue({ isFile: () => false });
388
+
389
+ await pushCommand.execute(defaultOptions);
390
+
391
+ expect(mockScanApiService.batchUpdateUpload).toHaveBeenCalledWith(
392
+ expect.any(String),
393
+ expect.arrayContaining([
394
+ expect.objectContaining({
395
+ id: 1,
396
+ uploaded: false,
397
+ uploadError: expect.stringContaining('NOT_A_FILE'),
398
+ }),
399
+ ])
400
+ );
401
+ });
402
+
403
+ it('should process multiple batches', async () => {
404
+ mockScanApiService.getPushStats.mockResolvedValue({
405
+ totalWithArelaPath: 200,
406
+ uploaded: 0,
407
+ pending: 200,
408
+ errors: 0,
409
+ });
410
+
411
+ mockScanApiService.fetchFilesForPush
412
+ .mockResolvedValueOnce([
413
+ {
414
+ id: 1,
415
+ absolute_path: '/test/file1.pdf',
416
+ arela_path: 'RFC/2023/3429/07/12345/',
417
+ file_name: 'file1.pdf',
418
+ rfc: 'RFC123',
419
+ },
420
+ ])
421
+ .mockResolvedValueOnce([]);
422
+
423
+ mockFetch.mockResolvedValue({
424
+ ok: true,
425
+ json: () => Promise.resolve({ uploaded: [{ id: 'storage-id' }] }),
426
+ });
427
+
428
+ await pushCommand.execute(defaultOptions);
429
+
430
+ // Should have fetched files at least twice (once with data, once empty)
431
+ expect(mockScanApiService.fetchFilesForPush).toHaveBeenCalled();
432
+ });
433
+ });
434
+
435
+ describe('upload API', () => {
436
+ const defaultOptions = {
437
+ batchSize: '100',
438
+ uploadBatchSize: '10',
439
+ };
440
+
441
+ beforeEach(() => {
442
+ mockScanApiService.getPushStats.mockResolvedValue({
443
+ totalWithArelaPath: 1,
444
+ uploaded: 0,
445
+ pending: 1,
446
+ errors: 0,
447
+ });
448
+
449
+ mockScanApiService.fetchFilesForPush
450
+ .mockResolvedValueOnce([
451
+ {
452
+ id: 1,
453
+ absolute_path: '/test/file.pdf',
454
+ arela_path: 'RFC/2023/3429/07/12345/',
455
+ file_name: 'file.pdf',
456
+ rfc: 'RFC123',
457
+ detected_pedimento_year: 2023,
458
+ relative_path: 'file.pdf',
459
+ },
460
+ ])
461
+ .mockResolvedValueOnce([]);
462
+ });
463
+
464
+ it('should call storage API with correct endpoint', async () => {
465
+ mockFetch.mockResolvedValue({
466
+ ok: true,
467
+ json: () => Promise.resolve({ uploaded: [{ id: 'storage-id' }] }),
468
+ });
469
+
470
+ await pushCommand.execute(defaultOptions);
471
+
472
+ expect(mockFetch).toHaveBeenCalledWith(
473
+ expect.stringContaining('/api/storage/batch-upload-and-process'),
474
+ expect.any(Object)
475
+ );
476
+ });
477
+
478
+ it('should handle API upload failure', async () => {
479
+ mockScanApiService.getPushStats.mockResolvedValue({
480
+ totalWithArelaPath: 1,
481
+ uploaded: 0,
482
+ pending: 1,
483
+ errors: 0,
484
+ });
485
+
486
+ mockScanApiService.fetchFilesForPush
487
+ .mockResolvedValueOnce([
488
+ {
489
+ id: 1,
490
+ absolute_path: '/test/file.pdf',
491
+ arela_path: 'RFC/2023/3429/07/12345/',
492
+ file_name: 'file.pdf',
493
+ rfc: 'RFC123',
494
+ detected_pedimento_year: 2023,
495
+ relative_path: 'file.pdf',
496
+ },
497
+ ])
498
+ .mockResolvedValueOnce([]);
499
+
500
+ mockFetch.mockResolvedValue({
501
+ ok: false,
502
+ status: 500,
503
+ text: () => Promise.resolve('Internal Server Error'),
504
+ });
505
+
506
+ // Should complete without throwing
507
+ await expect(pushCommand.execute(defaultOptions)).resolves.not.toThrow();
508
+ });
509
+
510
+ it('should handle network errors', async () => {
511
+ mockFetch.mockRejectedValue(new Error('Network error'));
512
+
513
+ await pushCommand.execute(defaultOptions);
514
+
515
+ expect(mockScanApiService.batchUpdateUpload).toHaveBeenCalledWith(
516
+ expect.any(String),
517
+ expect.arrayContaining([
518
+ expect.objectContaining({
519
+ id: 1,
520
+ uploaded: false,
521
+ uploadError: expect.stringContaining('Network error'),
522
+ }),
523
+ ])
524
+ );
525
+ });
526
+
527
+ it('should handle empty upload response', async () => {
528
+ mockScanApiService.getPushStats.mockResolvedValue({
529
+ totalWithArelaPath: 1,
530
+ uploaded: 0,
531
+ pending: 1,
532
+ errors: 0,
533
+ });
534
+
535
+ mockScanApiService.fetchFilesForPush
536
+ .mockResolvedValueOnce([
537
+ {
538
+ id: 1,
539
+ absolute_path: '/test/file.pdf',
540
+ arela_path: 'RFC/2023/3429/07/12345/',
541
+ file_name: 'file.pdf',
542
+ rfc: 'RFC123',
543
+ detected_pedimento_year: 2023,
544
+ relative_path: 'file.pdf',
545
+ },
546
+ ])
547
+ .mockResolvedValueOnce([]);
548
+
549
+ mockFetch.mockResolvedValue({
550
+ ok: true,
551
+ json: () => Promise.resolve({ uploaded: [], errors: ['No files processed'] }),
552
+ });
553
+
554
+ // Should complete without throwing
555
+ await expect(pushCommand.execute(defaultOptions)).resolves.not.toThrow();
556
+ });
557
+ });
558
+
559
+ describe('statistics display', () => {
560
+ const defaultOptions = {
561
+ batchSize: '100',
562
+ uploadBatchSize: '10',
563
+ };
564
+
565
+ it('should display table status', async () => {
566
+ await pushCommand.execute(defaultOptions);
567
+
568
+ expect(mockConsoleLog).toHaveBeenCalledWith(
569
+ expect.stringContaining('Table Status')
570
+ );
571
+ });
572
+
573
+ it('should display RFC breakdown when available', async () => {
574
+ mockScanApiService.getPushStats.mockResolvedValue({
575
+ totalWithArelaPath: 100,
576
+ uploaded: 50,
577
+ pending: 0,
578
+ errors: 0,
579
+ byRfc: [
580
+ { rfc: 'RFC123', uploaded: 25, total: 50 },
581
+ { rfc: 'RFC456', uploaded: 25, total: 50 },
582
+ ],
583
+ });
584
+
585
+ await pushCommand.execute(defaultOptions);
586
+
587
+ expect(mockConsoleLog).toHaveBeenCalledWith(
588
+ expect.stringContaining('Top RFCs')
589
+ );
590
+ });
591
+
592
+ it('should show errors count when present', async () => {
593
+ mockScanApiService.getPushStats.mockResolvedValue({
594
+ totalWithArelaPath: 100,
595
+ uploaded: 90,
596
+ pending: 0,
597
+ errors: 10,
598
+ maxAttemptsReached: 0,
599
+ });
600
+
601
+ await pushCommand.execute(defaultOptions);
602
+
603
+ expect(mockConsoleLog).toHaveBeenCalledWith(
604
+ expect.stringContaining('Errors: 10')
605
+ );
606
+ });
607
+
608
+ it('should show max attempts reached when present', async () => {
609
+ mockScanApiService.getPushStats.mockResolvedValue({
610
+ totalWithArelaPath: 100,
611
+ uploaded: 95,
612
+ pending: 0,
613
+ errors: 0,
614
+ maxAttemptsReached: 5,
615
+ });
616
+
617
+ await pushCommand.execute(defaultOptions);
618
+
619
+ expect(mockConsoleLog).toHaveBeenCalledWith(
620
+ expect.stringContaining('Max Attempts Reached: 5')
621
+ );
622
+ });
623
+
624
+ it('should display final results', async () => {
625
+ await pushCommand.execute(defaultOptions);
626
+
627
+ expect(mockConsoleLog).toHaveBeenCalledWith(
628
+ expect.stringContaining('Push Complete')
629
+ );
630
+ expect(mockConsoleLog).toHaveBeenCalledWith(
631
+ expect.stringContaining('Total Results')
632
+ );
633
+ });
634
+ });
635
+
636
+ describe('progress tracking', () => {
637
+ const defaultOptions = {
638
+ batchSize: '100',
639
+ uploadBatchSize: '10',
640
+ };
641
+
642
+ it('should create progress bar with correct total', async () => {
643
+ mockScanApiService.getPushStats.mockResolvedValue({
644
+ totalWithArelaPath: 100,
645
+ uploaded: 0,
646
+ pending: 100,
647
+ errors: 0,
648
+ });
649
+
650
+ mockScanApiService.fetchFilesForPush.mockResolvedValue([]);
651
+
652
+ await pushCommand.execute(defaultOptions);
653
+
654
+ expect(mockProgressBar.start).toHaveBeenCalledWith(
655
+ 100,
656
+ 0,
657
+ expect.any(Object)
658
+ );
659
+ });
660
+
661
+ it('should update progress during processing', async () => {
662
+ mockScanApiService.getPushStats.mockResolvedValue({
663
+ totalWithArelaPath: 2,
664
+ uploaded: 0,
665
+ pending: 2,
666
+ errors: 0,
667
+ });
668
+
669
+ mockScanApiService.fetchFilesForPush
670
+ .mockResolvedValueOnce([
671
+ {
672
+ id: 1,
673
+ absolute_path: '/test/file1.pdf',
674
+ arela_path: 'RFC/2023/3429/07/12345/',
675
+ file_name: 'file1.pdf',
676
+ rfc: 'RFC123',
677
+ },
678
+ ])
679
+ .mockResolvedValueOnce([]);
680
+
681
+ mockFetch.mockResolvedValue({
682
+ ok: true,
683
+ json: () => Promise.resolve({ uploaded: [{ id: 'storage-id' }] }),
684
+ });
685
+
686
+ await pushCommand.execute(defaultOptions);
687
+
688
+ // Progress bar should have been started
689
+ expect(mockProgressBar.start).toHaveBeenCalled();
690
+ });
691
+ });
692
+
693
+ describe('error handling', () => {
694
+ const defaultOptions = {
695
+ batchSize: '100',
696
+ uploadBatchSize: '10',
697
+ };
698
+
699
+ it('should handle unexpected errors', async () => {
700
+ mockScanApiService.getInstanceTables.mockRejectedValue(
701
+ new Error('Unexpected error')
702
+ );
703
+
704
+ await pushCommand.execute(defaultOptions);
705
+
706
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
707
+ expect(mockConsoleError).toHaveBeenCalledWith(
708
+ expect.stringContaining('Push failed'),
709
+ expect.stringContaining('Unexpected error')
710
+ );
711
+ });
712
+
713
+ it('should log detailed error information', async () => {
714
+ mockScanApiService.getPushStats.mockResolvedValue({
715
+ totalWithArelaPath: 10,
716
+ uploaded: 0,
717
+ pending: 10,
718
+ errors: 0,
719
+ });
720
+
721
+ mockScanApiService.fetchFilesForPush.mockRejectedValue(
722
+ new Error('Database connection failed')
723
+ );
724
+
725
+ await pushCommand.execute(defaultOptions);
726
+
727
+ // Fetch should have been attempted
728
+ expect(mockScanApiService.fetchFilesForPush).toHaveBeenCalled();
729
+ });
730
+ });
731
+
732
+ describe('configuration validation', () => {
733
+ const defaultOptions = {
734
+ batchSize: '100',
735
+ uploadBatchSize: '10',
736
+ };
737
+
738
+ it('should report missing table name', async () => {
739
+ mockAppConfig.getScanConfig.mockReturnValue({
740
+ companySlug: 'test-company',
741
+ serverId: 'test-server',
742
+ basePathFull: '/test/path',
743
+ tableName: null, // Missing table name
744
+ });
745
+
746
+ await pushCommand.execute(defaultOptions);
747
+
748
+ expect(mockConsoleError).toHaveBeenCalledWith(
749
+ expect.stringContaining('Configuration errors')
750
+ );
751
+ expect(mockProcessExit).toHaveBeenCalledWith(1);
752
+ });
753
+ });
754
+ });