@arela/uploader 1.0.2 → 1.0.4
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/.env.template +70 -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/API_RETRY_MECHANISM.md +338 -0
- package/docs/ARELA_IDENTIFY_IMPLEMENTATION.md +489 -0
- package/docs/ARELA_IDENTIFY_QUICKREF.md +186 -0
- package/docs/ARELA_PROPAGATE_IMPLEMENTATION.md +581 -0
- package/docs/ARELA_PROPAGATE_QUICKREF.md +272 -0
- package/docs/ARELA_PUSH_IMPLEMENTATION.md +577 -0
- package/docs/ARELA_PUSH_QUICKREF.md +322 -0
- package/docs/ARELA_SCAN_IMPLEMENTATION.md +373 -0
- package/docs/ARELA_SCAN_QUICKREF.md +139 -0
- package/docs/CROSS_PLATFORM_PATH_HANDLING.md +593 -0
- package/docs/DETECTION_ATTEMPT_TRACKING.md +414 -0
- package/docs/MIGRATION_UPLOADER_TO_FILE_STATS.md +1020 -0
- package/docs/MULTI_LEVEL_DIRECTORY_SCANNING.md +494 -0
- package/docs/STATS_COMMAND_SEQUENCE_DIAGRAM.md +287 -0
- package/docs/STATS_COMMAND_SIMPLE.md +93 -0
- package/package.json +31 -3
- package/src/commands/IdentifyCommand.js +459 -0
- package/src/commands/PropagateCommand.js +474 -0
- package/src/commands/PushCommand.js +473 -0
- package/src/commands/ScanCommand.js +523 -0
- package/src/config/config.js +154 -7
- package/src/file-detection.js +9 -10
- package/src/index.js +150 -0
- package/src/services/ScanApiService.js +645 -0
- package/src/utils/PathNormalizer.js +220 -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,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for ScanCommand
|
|
3
|
+
* Tests the arela scan command functionality
|
|
4
|
+
*/
|
|
5
|
+
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
6
|
+
|
|
7
|
+
// Mock dependencies before importing ScanCommand
|
|
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
|
+
startTime: Date.now(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const mockCliProgress = {
|
|
24
|
+
SingleBar: jest.fn(() => mockProgressBar),
|
|
25
|
+
Presets: {
|
|
26
|
+
shades_classic: {},
|
|
27
|
+
legacy: {},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const mockScanApiService = {
|
|
32
|
+
registerInstance: jest.fn(),
|
|
33
|
+
batchInsertStats: jest.fn(),
|
|
34
|
+
completeScan: jest.fn(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mockAppConfig = {
|
|
38
|
+
validateScanConfig: jest.fn(),
|
|
39
|
+
getScanConfig: jest.fn(),
|
|
40
|
+
getBasePath: jest.fn(),
|
|
41
|
+
getUploadSources: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const mockPathNormalizer = {
|
|
45
|
+
toAbsolutePath: jest.fn((p) => `/absolute/${p}`),
|
|
46
|
+
normalizeSeparators: jest.fn((p) => p.replace(/\\/g, '/')),
|
|
47
|
+
getRelativePath: jest.fn((file, base) => file.replace(base, '')),
|
|
48
|
+
generateTableName: jest.fn(() => 'scan_test_table'),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const mockGlobbyStream = jest.fn();
|
|
52
|
+
|
|
53
|
+
jest.unstable_mockModule('cli-progress', () => ({
|
|
54
|
+
default: mockCliProgress,
|
|
55
|
+
SingleBar: mockCliProgress.SingleBar,
|
|
56
|
+
Presets: mockCliProgress.Presets,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
jest.unstable_mockModule('../../src/services/LoggingService.js', () => ({
|
|
60
|
+
default: mockLogger,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
jest.unstable_mockModule('../../src/services/ScanApiService.js', () => ({
|
|
64
|
+
default: jest.fn(() => mockScanApiService),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
jest.unstable_mockModule('../../src/config/config.js', () => ({
|
|
68
|
+
default: mockAppConfig,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
jest.unstable_mockModule('../../src/utils/PathNormalizer.js', () => ({
|
|
72
|
+
default: mockPathNormalizer,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
jest.unstable_mockModule('globby', () => ({
|
|
76
|
+
globbyStream: mockGlobbyStream,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
jest.unstable_mockModule('../../src/errors/ErrorHandler.js', () => ({
|
|
80
|
+
default: jest.fn(() => ({
|
|
81
|
+
handleError: jest.fn(),
|
|
82
|
+
})),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
jest.unstable_mockModule('../../src/errors/ErrorTypes.js', () => ({
|
|
86
|
+
ConfigurationError: class ConfigurationError extends Error {
|
|
87
|
+
constructor(message) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = 'ConfigurationError';
|
|
90
|
+
this.errors = [];
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// Import the class after mocking
|
|
96
|
+
const { ScanCommand } = await import('../../src/commands/ScanCommand.js');
|
|
97
|
+
|
|
98
|
+
describe('ScanCommand', () => {
|
|
99
|
+
let scanCommand;
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
jest.clearAllMocks();
|
|
103
|
+
|
|
104
|
+
// Default mock implementations
|
|
105
|
+
mockAppConfig.getScanConfig.mockReturnValue({
|
|
106
|
+
companySlug: 'test-company',
|
|
107
|
+
serverId: 'test-server',
|
|
108
|
+
directoryLevel: 0,
|
|
109
|
+
excludePatterns: ['.DS_Store', 'Thumbs.db'],
|
|
110
|
+
batchSize: 2000,
|
|
111
|
+
});
|
|
112
|
+
mockAppConfig.getBasePath.mockReturnValue('./test-path');
|
|
113
|
+
mockAppConfig.getUploadSources.mockReturnValue(['.']);
|
|
114
|
+
mockAppConfig.validateScanConfig.mockReturnValue(undefined);
|
|
115
|
+
|
|
116
|
+
mockScanApiService.registerInstance.mockResolvedValue({
|
|
117
|
+
tableName: 'scan_test_table',
|
|
118
|
+
existed: false,
|
|
119
|
+
});
|
|
120
|
+
mockScanApiService.batchInsertStats.mockResolvedValue({ inserted: 10 });
|
|
121
|
+
mockScanApiService.completeScan.mockResolvedValue({ success: true });
|
|
122
|
+
|
|
123
|
+
scanCommand = new ScanCommand();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
jest.restoreAllMocks();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('constructor', () => {
|
|
131
|
+
it('should create a new ScanCommand instance', () => {
|
|
132
|
+
expect(scanCommand).toBeInstanceOf(ScanCommand);
|
|
133
|
+
expect(scanCommand.scanApiService).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('execute', () => {
|
|
138
|
+
it('should validate scan configuration', async () => {
|
|
139
|
+
// Setup mock for empty file stream
|
|
140
|
+
mockGlobbyStream.mockReturnValue({
|
|
141
|
+
async *[Symbol.asyncIterator]() {
|
|
142
|
+
// Empty stream
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await scanCommand.execute({});
|
|
147
|
+
|
|
148
|
+
expect(mockAppConfig.validateScanConfig).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return success with stats when scan completes', async () => {
|
|
152
|
+
mockGlobbyStream.mockReturnValue({
|
|
153
|
+
async *[Symbol.asyncIterator]() {
|
|
154
|
+
// Empty stream for simplicity
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await scanCommand.execute({});
|
|
159
|
+
|
|
160
|
+
expect(result).toHaveProperty('success');
|
|
161
|
+
expect(result).toHaveProperty('stats');
|
|
162
|
+
expect(result.stats).toHaveProperty('filesScanned');
|
|
163
|
+
expect(result.stats).toHaveProperty('filesInserted');
|
|
164
|
+
expect(result.stats).toHaveProperty('filesSkipped');
|
|
165
|
+
expect(result.stats).toHaveProperty('totalSize');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should discover directories when directoryLevel is 0', async () => {
|
|
169
|
+
mockAppConfig.getScanConfig.mockReturnValue({
|
|
170
|
+
companySlug: 'test-company',
|
|
171
|
+
serverId: 'test-server',
|
|
172
|
+
directoryLevel: 0,
|
|
173
|
+
excludePatterns: [],
|
|
174
|
+
batchSize: 2000,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
mockGlobbyStream.mockReturnValue({
|
|
178
|
+
async *[Symbol.asyncIterator]() {
|
|
179
|
+
// Empty stream
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const result = await scanCommand.execute({});
|
|
184
|
+
|
|
185
|
+
expect(result.success).toBe(true);
|
|
186
|
+
expect(mockScanApiService.registerInstance).toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle configuration errors gracefully', async () => {
|
|
190
|
+
const { ConfigurationError } = await import('../../src/errors/ErrorTypes.js');
|
|
191
|
+
mockAppConfig.validateScanConfig.mockImplementation(() => {
|
|
192
|
+
throw new ConfigurationError('Missing required config');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await scanCommand.execute({});
|
|
196
|
+
|
|
197
|
+
expect(result.success).toBe(false);
|
|
198
|
+
expect(result).toHaveProperty('error');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should register instances for each discovered directory', async () => {
|
|
202
|
+
mockAppConfig.getUploadSources.mockReturnValue(['.', 'subdir']);
|
|
203
|
+
|
|
204
|
+
mockGlobbyStream.mockReturnValue({
|
|
205
|
+
async *[Symbol.asyncIterator]() {
|
|
206
|
+
// Empty stream
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await scanCommand.execute({});
|
|
211
|
+
|
|
212
|
+
expect(mockScanApiService.registerInstance).toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should complete scan after processing', async () => {
|
|
216
|
+
mockGlobbyStream.mockReturnValue({
|
|
217
|
+
async *[Symbol.asyncIterator]() {
|
|
218
|
+
// Empty stream
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await scanCommand.execute({});
|
|
223
|
+
|
|
224
|
+
expect(mockScanApiService.completeScan).toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should return tables in result when successful', async () => {
|
|
228
|
+
mockScanApiService.registerInstance.mockResolvedValue({
|
|
229
|
+
tableName: 'scan_unique_table',
|
|
230
|
+
existed: false,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
mockGlobbyStream.mockReturnValue({
|
|
234
|
+
async *[Symbol.asyncIterator]() {
|
|
235
|
+
// Empty stream
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const result = await scanCommand.execute({});
|
|
240
|
+
|
|
241
|
+
expect(result.success).toBe(true);
|
|
242
|
+
expect(result.tables).toContain('scan_unique_table');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle countFirst option', async () => {
|
|
246
|
+
mockGlobbyStream.mockReturnValue({
|
|
247
|
+
async *[Symbol.asyncIterator]() {
|
|
248
|
+
// Empty stream
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const result = await scanCommand.execute({ countFirst: true });
|
|
253
|
+
|
|
254
|
+
expect(result.success).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('error handling', () => {
|
|
259
|
+
it('should handle API errors during registration', async () => {
|
|
260
|
+
mockScanApiService.registerInstance.mockRejectedValue(
|
|
261
|
+
new Error('API connection failed')
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const result = await scanCommand.execute({});
|
|
265
|
+
|
|
266
|
+
expect(result.success).toBe(false);
|
|
267
|
+
expect(result.error).toBe('API connection failed');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle batch insert failures', async () => {
|
|
271
|
+
mockScanApiService.batchInsertStats.mockRejectedValue(
|
|
272
|
+
new Error('Database error')
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
mockGlobbyStream.mockReturnValue({
|
|
276
|
+
async *[Symbol.asyncIterator]() {
|
|
277
|
+
yield { path: '/test/file.txt', stats: { size: 100, mtime: new Date() } };
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const result = await scanCommand.execute({});
|
|
282
|
+
|
|
283
|
+
// Command should still complete but with errors
|
|
284
|
+
expect(result).toBeDefined();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should return zero stats on failure', async () => {
|
|
288
|
+
mockAppConfig.validateScanConfig.mockImplementation(() => {
|
|
289
|
+
throw new Error('Config validation failed');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const result = await scanCommand.execute({});
|
|
293
|
+
|
|
294
|
+
expect(result.success).toBe(false);
|
|
295
|
+
expect(result.stats.filesScanned).toBe(0);
|
|
296
|
+
expect(result.stats.filesInserted).toBe(0);
|
|
297
|
+
expect(result.stats.filesSkipped).toBe(0);
|
|
298
|
+
expect(result.stats.totalSize).toBe(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('file exclusion', () => {
|
|
303
|
+
it('should exclude files matching exclude patterns', async () => {
|
|
304
|
+
mockAppConfig.getScanConfig.mockReturnValue({
|
|
305
|
+
companySlug: 'test-company',
|
|
306
|
+
serverId: 'test-server',
|
|
307
|
+
directoryLevel: 0,
|
|
308
|
+
excludePatterns: ['.DS_Store', '*.tmp'],
|
|
309
|
+
batchSize: 2000,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
mockGlobbyStream.mockReturnValue({
|
|
313
|
+
async *[Symbol.asyncIterator]() {
|
|
314
|
+
yield { path: '/test/.DS_Store', stats: { size: 100, mtime: new Date() } };
|
|
315
|
+
yield { path: '/test/valid.pdf', stats: { size: 200, mtime: new Date() } };
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const result = await scanCommand.execute({});
|
|
320
|
+
|
|
321
|
+
expect(result).toBeDefined();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('progress tracking', () => {
|
|
326
|
+
it('should create progress bar during scan', async () => {
|
|
327
|
+
mockGlobbyStream.mockReturnValue({
|
|
328
|
+
async *[Symbol.asyncIterator]() {
|
|
329
|
+
yield { path: '/test/file1.pdf', stats: { size: 100, mtime: new Date() } };
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await scanCommand.execute({});
|
|
334
|
+
|
|
335
|
+
expect(mockCliProgress.SingleBar).toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should stop progress bar after scan completes', async () => {
|
|
339
|
+
mockGlobbyStream.mockReturnValue({
|
|
340
|
+
async *[Symbol.asyncIterator]() {
|
|
341
|
+
// Empty stream
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await scanCommand.execute({});
|
|
346
|
+
|
|
347
|
+
expect(mockProgressBar.stop).toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('batch processing', () => {
|
|
352
|
+
it('should process files in batches', async () => {
|
|
353
|
+
const fileCount = 5;
|
|
354
|
+
const files = Array.from({ length: fileCount }, (_, i) => ({
|
|
355
|
+
path: `/test/file${i}.pdf`,
|
|
356
|
+
stats: { size: 100, mtime: new Date() },
|
|
357
|
+
}));
|
|
358
|
+
|
|
359
|
+
mockGlobbyStream.mockReturnValue({
|
|
360
|
+
async *[Symbol.asyncIterator]() {
|
|
361
|
+
for (const file of files) {
|
|
362
|
+
yield file;
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
mockAppConfig.getScanConfig.mockReturnValue({
|
|
368
|
+
companySlug: 'test-company',
|
|
369
|
+
serverId: 'test-server',
|
|
370
|
+
directoryLevel: 0,
|
|
371
|
+
excludePatterns: [],
|
|
372
|
+
batchSize: 2, // Small batch size to test batching
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const result = await scanCommand.execute({});
|
|
376
|
+
|
|
377
|
+
expect(result.success).toBe(true);
|
|
378
|
+
// Should have called batchInsertStats multiple times due to small batch size
|
|
379
|
+
expect(mockScanApiService.batchInsertStats).toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|