@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,570 @@
1
+ /**
2
+ * Unit tests for IdentifyCommand
3
+ * Tests the arela identify command functionality
4
+ */
5
+ import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
6
+
7
+ // Mock dependencies before importing IdentifyCommand
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
+ getDetectionStats: jest.fn(),
32
+ fetchPdfsForDetection: jest.fn(),
33
+ batchUpdateDetection: jest.fn(),
34
+ };
35
+
36
+ const mockAppConfig = {
37
+ validateScanConfig: jest.fn(),
38
+ getScanConfig: jest.fn(),
39
+ getBasePath: jest.fn(),
40
+ setApiTarget: jest.fn(),
41
+ };
42
+
43
+ const mockFileDetectionService = {
44
+ detectFile: jest.fn(),
45
+ };
46
+
47
+ const mockFs = {
48
+ existsSync: jest.fn(),
49
+ statSync: jest.fn(),
50
+ };
51
+
52
+ jest.unstable_mockModule('cli-progress', () => ({
53
+ default: mockCliProgress,
54
+ SingleBar: mockCliProgress.SingleBar,
55
+ Presets: mockCliProgress.Presets,
56
+ }));
57
+
58
+ jest.unstable_mockModule('fs', () => ({
59
+ default: mockFs,
60
+ existsSync: mockFs.existsSync,
61
+ statSync: mockFs.statSync,
62
+ }));
63
+
64
+ jest.unstable_mockModule('p-limit', () => ({
65
+ default: jest.fn(() => (fn) => fn()),
66
+ }));
67
+
68
+ jest.unstable_mockModule('../../src/services/LoggingService.js', () => ({
69
+ default: mockLogger,
70
+ }));
71
+
72
+ jest.unstable_mockModule('../../src/services/ScanApiService.js', () => ({
73
+ default: jest.fn(() => mockScanApiService),
74
+ }));
75
+
76
+ jest.unstable_mockModule('../../src/config/config.js', () => ({
77
+ default: mockAppConfig,
78
+ }));
79
+
80
+ jest.unstable_mockModule('../../src/file-detection.js', () => ({
81
+ default: jest.fn(() => mockFileDetectionService),
82
+ FileDetectionService: jest.fn(() => mockFileDetectionService),
83
+ }));
84
+
85
+ jest.unstable_mockModule('../../src/errors/ErrorHandler.js', () => ({
86
+ default: jest.fn(() => ({
87
+ handleError: jest.fn(),
88
+ })),
89
+ }));
90
+
91
+ jest.unstable_mockModule('../../src/errors/ErrorTypes.js', () => ({
92
+ ConfigurationError: class ConfigurationError extends Error {
93
+ constructor(message) {
94
+ super(message);
95
+ this.name = 'ConfigurationError';
96
+ this.errors = [];
97
+ }
98
+ },
99
+ }));
100
+
101
+ // Import the class after mocking
102
+ const { IdentifyCommand } = await import('../../src/commands/IdentifyCommand.js');
103
+
104
+ describe('IdentifyCommand', () => {
105
+ let identifyCommand;
106
+
107
+ beforeEach(() => {
108
+ jest.clearAllMocks();
109
+
110
+ // Default mock implementations
111
+ mockAppConfig.getScanConfig.mockReturnValue({
112
+ companySlug: 'test-company',
113
+ serverId: 'test-server',
114
+ basePathFull: '/test/path',
115
+ });
116
+ mockAppConfig.getBasePath.mockReturnValue('/test/path');
117
+ mockAppConfig.validateScanConfig.mockReturnValue(undefined);
118
+
119
+ mockScanApiService.getInstanceTables.mockResolvedValue([
120
+ { tableName: 'scan_test_table' },
121
+ ]);
122
+
123
+ mockScanApiService.getDetectionStats.mockResolvedValue({
124
+ totalPdfs: 100,
125
+ detected: 50,
126
+ pending: 50,
127
+ notPedimento: 0,
128
+ maxAttemptsReached: 0,
129
+ errors: 0,
130
+ });
131
+
132
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
133
+ data: [],
134
+ hasMore: false,
135
+ });
136
+
137
+ mockScanApiService.batchUpdateDetection.mockResolvedValue({
138
+ updated: 0,
139
+ errors: 0,
140
+ });
141
+
142
+ mockFs.existsSync.mockReturnValue(true);
143
+ mockFs.statSync.mockReturnValue({ size: 1000, isFile: () => true });
144
+
145
+ identifyCommand = new IdentifyCommand();
146
+ });
147
+
148
+ afterEach(() => {
149
+ jest.restoreAllMocks();
150
+ });
151
+
152
+ describe('constructor', () => {
153
+ it('should create a new IdentifyCommand instance', () => {
154
+ expect(identifyCommand).toBeInstanceOf(IdentifyCommand);
155
+ expect(identifyCommand.scanApiService).toBeNull();
156
+ });
157
+
158
+ it('should initialize detection service', () => {
159
+ expect(identifyCommand.detectionService).toBeDefined();
160
+ });
161
+ });
162
+
163
+ describe('execute', () => {
164
+ it('should validate scan configuration', async () => {
165
+ await identifyCommand.execute({});
166
+
167
+ expect(mockAppConfig.validateScanConfig).toHaveBeenCalled();
168
+ });
169
+
170
+ it('should fetch instance tables', async () => {
171
+ await identifyCommand.execute({});
172
+
173
+ expect(mockScanApiService.getInstanceTables).toHaveBeenCalledWith(
174
+ 'test-company',
175
+ 'test-server',
176
+ '/test/path'
177
+ );
178
+ });
179
+
180
+ it('should throw error when no tables found', async () => {
181
+ mockScanApiService.getInstanceTables.mockResolvedValue([]);
182
+
183
+ await expect(identifyCommand.execute({})).rejects.toThrow(
184
+ 'No tables found for this instance'
185
+ );
186
+ });
187
+
188
+ it('should set API target when specified', async () => {
189
+ await identifyCommand.execute({ api: 'agencia' });
190
+
191
+ expect(mockAppConfig.setApiTarget).toHaveBeenCalledWith('agencia');
192
+ });
193
+
194
+ it('should use default batch size of 100', async () => {
195
+ await identifyCommand.execute({});
196
+
197
+ // Logger should show batch size
198
+ expect(mockLogger.info).toHaveBeenCalledWith(
199
+ expect.stringContaining('Batch Size: 100')
200
+ );
201
+ });
202
+
203
+ it('should use custom batch size when provided', async () => {
204
+ await identifyCommand.execute({ batchSize: 50 });
205
+
206
+ expect(mockLogger.info).toHaveBeenCalledWith(
207
+ expect.stringContaining('Batch Size: 50')
208
+ );
209
+ });
210
+
211
+ it('should skip table when all PDFs are processed', async () => {
212
+ mockScanApiService.getDetectionStats.mockResolvedValue({
213
+ totalPdfs: 100,
214
+ detected: 100,
215
+ pending: 0,
216
+ notPedimento: 0,
217
+ maxAttemptsReached: 0,
218
+ errors: 0,
219
+ });
220
+
221
+ await identifyCommand.execute({});
222
+
223
+ expect(mockLogger.info).toHaveBeenCalledWith(
224
+ expect.stringContaining('All PDFs processed')
225
+ );
226
+ });
227
+
228
+ it('should process pending PDFs', async () => {
229
+ mockScanApiService.getDetectionStats.mockResolvedValue({
230
+ totalPdfs: 10,
231
+ detected: 5,
232
+ pending: 5,
233
+ notPedimento: 0,
234
+ maxAttemptsReached: 0,
235
+ errors: 0,
236
+ });
237
+
238
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
239
+ data: [
240
+ { id: 1, absolute_path: '/test/file1.pdf', relative_path: 'file1.pdf' },
241
+ { id: 2, absolute_path: '/test/file2.pdf', relative_path: 'file2.pdf' },
242
+ ],
243
+ hasMore: false,
244
+ });
245
+
246
+ mockFileDetectionService.detectFile.mockResolvedValue({
247
+ detectedType: 'pedimento_simplificado',
248
+ detectedPedimento: '12345678',
249
+ detectedPedimentoYear: 2023,
250
+ rfc: 'RFC123456ABC',
251
+ arelaPath: 'RFC123456ABC/2023/3429/07/12345678/',
252
+ });
253
+
254
+ await identifyCommand.execute({});
255
+
256
+ expect(mockScanApiService.batchUpdateDetection).toHaveBeenCalled();
257
+ });
258
+ });
259
+
260
+ describe('file detection', () => {
261
+ beforeEach(() => {
262
+ mockScanApiService.getDetectionStats.mockResolvedValue({
263
+ totalPdfs: 1,
264
+ detected: 0,
265
+ pending: 1,
266
+ notPedimento: 0,
267
+ maxAttemptsReached: 0,
268
+ errors: 0,
269
+ });
270
+ });
271
+
272
+ it('should detect pedimento files', async () => {
273
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
274
+ data: [
275
+ { id: 1, absolute_path: '/test/pedimento.pdf', relative_path: 'pedimento.pdf' },
276
+ ],
277
+ hasMore: false,
278
+ });
279
+
280
+ mockFileDetectionService.detectFile.mockResolvedValue({
281
+ detectedType: 'pedimento_simplificado',
282
+ detectedPedimento: '12345678',
283
+ detectedPedimentoYear: 2023,
284
+ rfc: 'RFC123456ABC',
285
+ arelaPath: 'RFC123456ABC/2023/3429/07/12345678/',
286
+ });
287
+
288
+ await identifyCommand.execute({});
289
+
290
+ expect(mockFileDetectionService.detectFile).toHaveBeenCalledWith(
291
+ '/test/pedimento.pdf'
292
+ );
293
+ });
294
+
295
+ it('should handle file not found errors', async () => {
296
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
297
+ data: [
298
+ { id: 1, absolute_path: '/test/missing.pdf', relative_path: 'missing.pdf' },
299
+ ],
300
+ hasMore: false,
301
+ });
302
+
303
+ mockFs.existsSync.mockReturnValue(false);
304
+
305
+ await identifyCommand.execute({});
306
+
307
+ expect(mockScanApiService.batchUpdateDetection).toHaveBeenCalledWith(
308
+ expect.any(String),
309
+ expect.arrayContaining([
310
+ expect.objectContaining({
311
+ id: 1,
312
+ detectionError: expect.stringContaining('FILE_NOT_FOUND'),
313
+ }),
314
+ ])
315
+ );
316
+ });
317
+
318
+ it('should skip files exceeding max size', async () => {
319
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
320
+ data: [
321
+ { id: 1, absolute_path: '/test/large.pdf', relative_path: 'large.pdf' },
322
+ ],
323
+ hasMore: false,
324
+ });
325
+
326
+ // 60MB file (exceeds 50MB limit)
327
+ mockFs.statSync.mockReturnValue({ size: 60 * 1024 * 1024 });
328
+
329
+ await identifyCommand.execute({});
330
+
331
+ expect(mockScanApiService.batchUpdateDetection).toHaveBeenCalledWith(
332
+ expect.any(String),
333
+ expect.arrayContaining([
334
+ expect.objectContaining({
335
+ id: 1,
336
+ detectionError: expect.stringContaining('FILE_TOO_LARGE'),
337
+ }),
338
+ ])
339
+ );
340
+ });
341
+
342
+ it('should handle detection service errors', async () => {
343
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
344
+ data: [
345
+ { id: 1, absolute_path: '/test/corrupt.pdf', relative_path: 'corrupt.pdf' },
346
+ ],
347
+ hasMore: false,
348
+ });
349
+
350
+ mockFileDetectionService.detectFile.mockRejectedValue(
351
+ new Error('PDF parsing failed')
352
+ );
353
+
354
+ await identifyCommand.execute({});
355
+
356
+ expect(mockScanApiService.batchUpdateDetection).toHaveBeenCalledWith(
357
+ expect.any(String),
358
+ expect.arrayContaining([
359
+ expect.objectContaining({
360
+ id: 1,
361
+ detectionError: expect.stringContaining('PDF parsing failed'),
362
+ }),
363
+ ])
364
+ );
365
+ });
366
+ });
367
+
368
+ describe('batch processing', () => {
369
+ it('should process multiple batches when hasMore is true', async () => {
370
+ mockScanApiService.getDetectionStats.mockResolvedValue({
371
+ totalPdfs: 200,
372
+ detected: 0,
373
+ pending: 200,
374
+ notPedimento: 0,
375
+ maxAttemptsReached: 0,
376
+ errors: 0,
377
+ });
378
+
379
+ // First call returns files with hasMore=true
380
+ mockScanApiService.fetchPdfsForDetection
381
+ .mockResolvedValueOnce({
382
+ data: [{ id: 1, absolute_path: '/test/file1.pdf', relative_path: 'file1.pdf' }],
383
+ hasMore: true,
384
+ })
385
+ .mockResolvedValueOnce({
386
+ data: [{ id: 2, absolute_path: '/test/file2.pdf', relative_path: 'file2.pdf' }],
387
+ hasMore: false,
388
+ });
389
+
390
+ mockFileDetectionService.detectFile.mockResolvedValue({
391
+ detectedType: null,
392
+ text: '',
393
+ });
394
+
395
+ await identifyCommand.execute({});
396
+
397
+ expect(mockScanApiService.fetchPdfsForDetection).toHaveBeenCalledTimes(2);
398
+ });
399
+
400
+ it('should stop processing when no more files', async () => {
401
+ mockScanApiService.getDetectionStats.mockResolvedValue({
402
+ totalPdfs: 10,
403
+ detected: 0,
404
+ pending: 10,
405
+ notPedimento: 0,
406
+ maxAttemptsReached: 0,
407
+ errors: 0,
408
+ });
409
+
410
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
411
+ data: [],
412
+ hasMore: false,
413
+ });
414
+
415
+ await identifyCommand.execute({});
416
+
417
+ expect(mockScanApiService.fetchPdfsForDetection).toHaveBeenCalledTimes(1);
418
+ });
419
+ });
420
+
421
+ describe('progress tracking', () => {
422
+ it('should create progress bar for pending PDFs', async () => {
423
+ mockScanApiService.getDetectionStats.mockResolvedValue({
424
+ totalPdfs: 10,
425
+ detected: 0,
426
+ pending: 10,
427
+ notPedimento: 0,
428
+ maxAttemptsReached: 0,
429
+ errors: 0,
430
+ });
431
+
432
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
433
+ data: [{ id: 1, absolute_path: '/test/file.pdf', relative_path: 'file.pdf' }],
434
+ hasMore: false,
435
+ });
436
+
437
+ mockFileDetectionService.detectFile.mockResolvedValue({
438
+ detectedType: null,
439
+ text: '',
440
+ });
441
+
442
+ await identifyCommand.execute({});
443
+
444
+ expect(mockCliProgress.SingleBar).toHaveBeenCalled();
445
+ expect(mockProgressBar.start).toHaveBeenCalled();
446
+ expect(mockProgressBar.stop).toHaveBeenCalled();
447
+ });
448
+ });
449
+
450
+ describe('statistics', () => {
451
+ it('should show detailed stats when showStats option is enabled', async () => {
452
+ mockScanApiService.getDetectionStats.mockResolvedValue({
453
+ totalPdfs: 10,
454
+ detected: 5,
455
+ pending: 0,
456
+ notPedimento: 5,
457
+ maxAttemptsReached: 0,
458
+ errors: 0,
459
+ });
460
+
461
+ await identifyCommand.execute({ showStats: true });
462
+
463
+ expect(mockLogger.info).toHaveBeenCalledWith(
464
+ expect.stringContaining('Detailed Statistics')
465
+ );
466
+ });
467
+
468
+ it('should log max attempts reached warning', async () => {
469
+ mockScanApiService.getDetectionStats.mockResolvedValue({
470
+ totalPdfs: 10,
471
+ detected: 5,
472
+ pending: 0,
473
+ notPedimento: 3,
474
+ maxAttemptsReached: 2,
475
+ errors: 0,
476
+ });
477
+
478
+ await identifyCommand.execute({});
479
+
480
+ expect(mockLogger.info).toHaveBeenCalledWith(
481
+ expect.stringContaining('2 PDFs reached max attempts')
482
+ );
483
+ });
484
+ });
485
+
486
+ describe('error handling', () => {
487
+ it('should handle configuration errors', async () => {
488
+ const { ConfigurationError } = await import('../../src/errors/ErrorTypes.js');
489
+ mockAppConfig.validateScanConfig.mockImplementation(() => {
490
+ const error = new ConfigurationError('Missing config');
491
+ error.errors = ['Missing company slug'];
492
+ throw error;
493
+ });
494
+
495
+ await expect(identifyCommand.execute({})).rejects.toThrow('Missing config');
496
+ });
497
+
498
+ it('should handle API errors', async () => {
499
+ mockScanApiService.getInstanceTables.mockRejectedValue(
500
+ new Error('API connection failed')
501
+ );
502
+
503
+ await expect(identifyCommand.execute({})).rejects.toThrow(
504
+ 'API connection failed'
505
+ );
506
+ });
507
+ });
508
+
509
+ describe('pedimento detection logic', () => {
510
+ beforeEach(() => {
511
+ mockScanApiService.getDetectionStats.mockResolvedValue({
512
+ totalPdfs: 1,
513
+ detected: 0,
514
+ pending: 1,
515
+ notPedimento: 0,
516
+ maxAttemptsReached: 0,
517
+ errors: 0,
518
+ });
519
+
520
+ mockScanApiService.fetchPdfsForDetection.mockResolvedValue({
521
+ data: [{ id: 1, absolute_path: '/test/file.pdf', relative_path: 'file.pdf' }],
522
+ hasMore: false,
523
+ });
524
+ });
525
+
526
+ it('should mark file as not pedimento when marker not found', async () => {
527
+ mockFileDetectionService.detectFile.mockResolvedValue({
528
+ detectedType: null,
529
+ detectedPedimento: null,
530
+ detectedPedimentoYear: null,
531
+ rfc: null,
532
+ arelaPath: null,
533
+ text: 'Some random text without pedimento marker',
534
+ });
535
+
536
+ await identifyCommand.execute({});
537
+
538
+ expect(mockScanApiService.batchUpdateDetection).toHaveBeenCalledWith(
539
+ expect.any(String),
540
+ expect.arrayContaining([
541
+ expect.objectContaining({
542
+ isNotPedimento: true,
543
+ }),
544
+ ])
545
+ );
546
+ });
547
+
548
+ it('should not mark as not pedimento when has required marker', async () => {
549
+ mockFileDetectionService.detectFile.mockResolvedValue({
550
+ detectedType: null,
551
+ detectedPedimento: null,
552
+ detectedPedimentoYear: null,
553
+ rfc: null,
554
+ arelaPath: null,
555
+ text: 'FORMA SIMPLIFICADA DE PEDIMENTO but missing fields',
556
+ });
557
+
558
+ await identifyCommand.execute({});
559
+
560
+ expect(mockScanApiService.batchUpdateDetection).toHaveBeenCalledWith(
561
+ expect.any(String),
562
+ expect.arrayContaining([
563
+ expect.objectContaining({
564
+ isNotPedimento: false,
565
+ }),
566
+ ])
567
+ );
568
+ });
569
+ });
570
+ });