@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,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
+ });