@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.
- package/.env.local +316 -0
- package/coverage/IdentifyCommand.js.html +1462 -0
- package/coverage/PropagateCommand.js.html +1507 -0
- package/coverage/PushCommand.js.html +1504 -0
- package/coverage/ScanCommand.js.html +1654 -0
- package/coverage/UploadCommand.js.html +1846 -0
- package/coverage/WatchCommand.js.html +4111 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +191 -0
- package/coverage/lcov-report/IdentifyCommand.js.html +1462 -0
- package/coverage/lcov-report/PropagateCommand.js.html +1507 -0
- package/coverage/lcov-report/PushCommand.js.html +1504 -0
- package/coverage/lcov-report/ScanCommand.js.html +1654 -0
- package/coverage/lcov-report/UploadCommand.js.html +1846 -0
- package/coverage/lcov-report/WatchCommand.js.html +4111 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +191 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +1937 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/docs/CROSS_PLATFORM_PATH_HANDLING.md +597 -0
- package/package.json +28 -2
- package/src/commands/IdentifyCommand.js +1 -28
- package/src/commands/PropagateCommand.js +1 -1
- package/src/commands/PushCommand.js +1 -1
- package/src/commands/ScanCommand.js +27 -20
- package/src/config/config.js +27 -48
- package/src/services/ScanApiService.js +4 -5
- package/src/utils/PathNormalizer.js +272 -0
- package/tests/commands/IdentifyCommand.test.js +570 -0
- package/tests/commands/PropagateCommand.test.js +568 -0
- package/tests/commands/PushCommand.test.js +754 -0
- package/tests/commands/ScanCommand.test.js +382 -0
- 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
|
+
});
|