@rsweeten/dropbox-sync 0.1.2 → 0.1.3
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/.github/workflows/test-pr.yml +30 -0
- package/README.md +207 -1
- package/__mocks__/nuxt/app.js +20 -0
- package/dist/adapters/__tests__/angular.spec.d.ts +1 -0
- package/dist/adapters/__tests__/angular.spec.js +237 -0
- package/dist/adapters/__tests__/next.spec.d.ts +1 -0
- package/dist/adapters/__tests__/next.spec.js +179 -0
- package/dist/adapters/__tests__/nuxt.spec.d.ts +1 -0
- package/dist/adapters/__tests__/nuxt.spec.js +145 -0
- package/dist/adapters/__tests__/svelte.spec.d.ts +1 -0
- package/dist/adapters/__tests__/svelte.spec.js +149 -0
- package/dist/core/__tests__/auth.spec.d.ts +1 -0
- package/dist/core/__tests__/auth.spec.js +83 -0
- package/dist/core/__tests__/client.spec.d.ts +1 -0
- package/dist/core/__tests__/client.spec.js +102 -0
- package/dist/core/__tests__/socket.spec.d.ts +1 -0
- package/dist/core/__tests__/socket.spec.js +122 -0
- package/dist/core/__tests__/sync.spec.d.ts +1 -0
- package/dist/core/__tests__/sync.spec.js +375 -0
- package/dist/core/sync.js +30 -11
- package/jest.config.js +24 -0
- package/jest.setup.js +38 -0
- package/package.json +4 -1
- package/src/adapters/__tests__/angular.spec.ts +338 -0
- package/src/adapters/__tests__/next.spec.ts +240 -0
- package/src/adapters/__tests__/nuxt.spec.ts +185 -0
- package/src/adapters/__tests__/svelte.spec.ts +194 -0
- package/src/core/__tests__/auth.spec.ts +142 -0
- package/src/core/__tests__/client.spec.ts +128 -0
- package/src/core/__tests__/socket.spec.ts +153 -0
- package/src/core/__tests__/sync.spec.ts +508 -0
- package/src/core/sync.ts +53 -26
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createSocketMethods } from '../socket'
|
|
2
|
+
import { io, Socket } from 'socket.io-client'
|
|
3
|
+
|
|
4
|
+
// Mock socket.io-client
|
|
5
|
+
jest.mock('socket.io-client', () => {
|
|
6
|
+
const mockSocket = {
|
|
7
|
+
connected: false,
|
|
8
|
+
connect: jest.fn(),
|
|
9
|
+
disconnect: jest.fn(),
|
|
10
|
+
on: jest.fn(),
|
|
11
|
+
off: jest.fn(),
|
|
12
|
+
emit: jest.fn(),
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
io: jest.fn().mockReturnValue(mockSocket),
|
|
16
|
+
Socket: jest.fn(),
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('createSocketMethods', () => {
|
|
21
|
+
let socketMethods: ReturnType<typeof createSocketMethods>
|
|
22
|
+
const mockSocket = (io as jest.Mock)()
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks()
|
|
26
|
+
socketMethods = createSocketMethods()
|
|
27
|
+
|
|
28
|
+
// Reset the connected state
|
|
29
|
+
Object.defineProperty(mockSocket, 'connected', {
|
|
30
|
+
value: false,
|
|
31
|
+
writable: true,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Set up a global window.location.origin for testing
|
|
35
|
+
global.window = {
|
|
36
|
+
location: {
|
|
37
|
+
origin: 'http://test-origin.com',
|
|
38
|
+
},
|
|
39
|
+
} as any
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should create socket methods object', () => {
|
|
43
|
+
expect(socketMethods).toHaveProperty('connect')
|
|
44
|
+
expect(socketMethods).toHaveProperty('disconnect')
|
|
45
|
+
expect(socketMethods).toHaveProperty('on')
|
|
46
|
+
expect(socketMethods).toHaveProperty('off')
|
|
47
|
+
expect(socketMethods).toHaveProperty('emit')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should connect to socket server with default URL if in browser', () => {
|
|
51
|
+
socketMethods.connect()
|
|
52
|
+
|
|
53
|
+
// Should create socket with window.location.origin
|
|
54
|
+
expect(io).toHaveBeenCalledWith('http://test-origin.com')
|
|
55
|
+
// Should call connect() on the socket
|
|
56
|
+
expect(mockSocket.connect).toHaveBeenCalled()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should connect to socket server with localhost fallback if not in browser', () => {
|
|
60
|
+
global.window = undefined as any
|
|
61
|
+
socketMethods.connect()
|
|
62
|
+
|
|
63
|
+
// Should use localhost fallback URL
|
|
64
|
+
expect(io).toHaveBeenCalledWith('http://localhost:3000')
|
|
65
|
+
expect(mockSocket.connect).toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should not try to reconnect if already connected', () => {
|
|
69
|
+
// First connection
|
|
70
|
+
socketMethods.connect()
|
|
71
|
+
expect(io).toHaveBeenCalledTimes(1)
|
|
72
|
+
|
|
73
|
+
// Mark as connected
|
|
74
|
+
Object.defineProperty(mockSocket, 'connected', { value: true })
|
|
75
|
+
|
|
76
|
+
// Try connecting again
|
|
77
|
+
socketMethods.connect()
|
|
78
|
+
|
|
79
|
+
// Should not create a new socket
|
|
80
|
+
expect(io).toHaveBeenCalledTimes(1)
|
|
81
|
+
// But should still call connect()
|
|
82
|
+
expect(mockSocket.connect).toHaveBeenCalledTimes(1)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should disconnect from socket server', () => {
|
|
86
|
+
// Connect first
|
|
87
|
+
socketMethods.connect()
|
|
88
|
+
|
|
89
|
+
// Mark as connected
|
|
90
|
+
Object.defineProperty(mockSocket, 'connected', { value: true })
|
|
91
|
+
|
|
92
|
+
// Disconnect
|
|
93
|
+
socketMethods.disconnect()
|
|
94
|
+
|
|
95
|
+
// Should call disconnect() on the socket
|
|
96
|
+
expect(mockSocket.disconnect).toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should not try to disconnect if not connected', () => {
|
|
100
|
+
// No connection established
|
|
101
|
+
socketMethods.disconnect()
|
|
102
|
+
|
|
103
|
+
// Should not call disconnect()
|
|
104
|
+
expect(mockSocket.disconnect).not.toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should register event handler', () => {
|
|
108
|
+
// Connect first
|
|
109
|
+
socketMethods.connect()
|
|
110
|
+
|
|
111
|
+
const handleEvent = jest.fn()
|
|
112
|
+
socketMethods.on('test-event', handleEvent)
|
|
113
|
+
|
|
114
|
+
// Should register the handler
|
|
115
|
+
expect(mockSocket.on).toHaveBeenCalledWith('test-event', handleEvent)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should remove event handler', () => {
|
|
119
|
+
// Connect first
|
|
120
|
+
socketMethods.connect()
|
|
121
|
+
|
|
122
|
+
const handleEvent = jest.fn()
|
|
123
|
+
socketMethods.on('test-event', handleEvent)
|
|
124
|
+
socketMethods.off('test-event')
|
|
125
|
+
|
|
126
|
+
// Should remove the handler
|
|
127
|
+
expect(mockSocket.off).toHaveBeenCalledWith('test-event', handleEvent)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should emit event with arguments', () => {
|
|
131
|
+
// Connect first
|
|
132
|
+
socketMethods.connect()
|
|
133
|
+
|
|
134
|
+
const result = socketMethods.emit('test-event', 'arg1', { foo: 'bar' })
|
|
135
|
+
|
|
136
|
+
// Should emit the event with arguments
|
|
137
|
+
expect(mockSocket.emit).toHaveBeenCalledWith('test-event', 'arg1', {
|
|
138
|
+
foo: 'bar',
|
|
139
|
+
})
|
|
140
|
+
// Should return true indicating success
|
|
141
|
+
expect(result).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should return false when emitting without connection', () => {
|
|
145
|
+
// No connection established
|
|
146
|
+
const result = socketMethods.emit('test-event', 'arg1')
|
|
147
|
+
|
|
148
|
+
// Should not emit
|
|
149
|
+
expect(mockSocket.emit).not.toHaveBeenCalled()
|
|
150
|
+
// Should return false indicating failure
|
|
151
|
+
expect(result).toBe(false)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { createSyncMethods } from '../sync'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { SocketMethods } from '../types'
|
|
5
|
+
|
|
6
|
+
// Mock fs module
|
|
7
|
+
jest.mock('fs', () => ({
|
|
8
|
+
readdirSync: jest.fn(),
|
|
9
|
+
readFileSync: jest.fn(),
|
|
10
|
+
writeFileSync: jest.fn(),
|
|
11
|
+
mkdirSync: jest.fn(),
|
|
12
|
+
existsSync: jest.fn(),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
// Mock path module
|
|
16
|
+
jest.mock('path', () => ({
|
|
17
|
+
join: jest.fn((...args) => args.join('/')),
|
|
18
|
+
dirname: jest.fn((path) => path.split('/').slice(0, -1).join('/') || '/'),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
// Mock Dropbox client responses
|
|
22
|
+
const mockDropboxListFolderResponse = {
|
|
23
|
+
result: {
|
|
24
|
+
entries: [
|
|
25
|
+
{ '.tag': 'file', path_display: '/test.jpg' },
|
|
26
|
+
{ '.tag': 'file', path_display: '/images/photo.png' },
|
|
27
|
+
{ '.tag': 'folder', path_display: '/images' },
|
|
28
|
+
{ '.tag': 'file', path_display: '/doc.txt' }, // Not an image, should be filtered out
|
|
29
|
+
],
|
|
30
|
+
has_more: false,
|
|
31
|
+
cursor: 'mock-cursor',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mockDropboxListFolderContinueResponse = {
|
|
36
|
+
result: {
|
|
37
|
+
entries: [],
|
|
38
|
+
has_more: false,
|
|
39
|
+
cursor: 'mock-cursor-2',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mockDropboxDownloadResponse = {
|
|
44
|
+
result: {
|
|
45
|
+
fileBinary: 'mock-file-content',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create mock client
|
|
50
|
+
const mockDropboxClient = {
|
|
51
|
+
filesListFolder: jest.fn().mockResolvedValue(mockDropboxListFolderResponse),
|
|
52
|
+
filesListFolderContinue: jest
|
|
53
|
+
.fn()
|
|
54
|
+
.mockResolvedValue(mockDropboxListFolderContinueResponse),
|
|
55
|
+
filesDownload: jest.fn().mockResolvedValue(mockDropboxDownloadResponse),
|
|
56
|
+
filesUpload: jest.fn().mockResolvedValue({ result: {} }),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Mock Dropbox constructor
|
|
60
|
+
jest.mock('dropbox', () => ({
|
|
61
|
+
Dropbox: jest.fn().mockImplementation(() => mockDropboxClient),
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
// Create mock socket methods
|
|
65
|
+
const mockSocketMethods: SocketMethods = {
|
|
66
|
+
connect: jest.fn(),
|
|
67
|
+
disconnect: jest.fn(),
|
|
68
|
+
on: jest.fn(),
|
|
69
|
+
off: jest.fn(),
|
|
70
|
+
emit: jest.fn(),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create mock file system entries
|
|
74
|
+
const mockFileEntries = [
|
|
75
|
+
{
|
|
76
|
+
name: 'test.jpg',
|
|
77
|
+
isDirectory: () => false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'images',
|
|
81
|
+
isDirectory: () => true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'document.txt',
|
|
85
|
+
isDirectory: () => false,
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
const mockImagesEntries = [
|
|
90
|
+
{
|
|
91
|
+
name: 'photo.png',
|
|
92
|
+
isDirectory: () => false,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'subdir',
|
|
96
|
+
isDirectory: () => true,
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
const mockSubdirEntries = [
|
|
101
|
+
{
|
|
102
|
+
name: 'deep.gif',
|
|
103
|
+
isDirectory: () => false,
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
describe('Sync Methods', () => {
|
|
108
|
+
let syncMethods: ReturnType<typeof createSyncMethods>
|
|
109
|
+
const mockGetClient = jest.fn().mockReturnValue(mockDropboxClient)
|
|
110
|
+
const mockCredentials = {
|
|
111
|
+
clientId: 'test-client-id',
|
|
112
|
+
accessToken: 'test-token',
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
jest.clearAllMocks()
|
|
117
|
+
|
|
118
|
+
// Configure mock fs behavior
|
|
119
|
+
;(fs.readdirSync as jest.Mock).mockImplementation((dir) => {
|
|
120
|
+
if (dir.includes('images/subdir')) return mockSubdirEntries
|
|
121
|
+
if (dir.includes('images')) return mockImagesEntries
|
|
122
|
+
return mockFileEntries
|
|
123
|
+
})
|
|
124
|
+
;(fs.existsSync as jest.Mock).mockReturnValue(true)
|
|
125
|
+
;(fs.readFileSync as jest.Mock).mockReturnValue(
|
|
126
|
+
Buffer.from('test-content')
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
syncMethods = createSyncMethods(
|
|
130
|
+
mockGetClient,
|
|
131
|
+
mockCredentials,
|
|
132
|
+
mockSocketMethods
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('scanLocalFiles', () => {
|
|
137
|
+
it('should scan local directory and return image files', async () => {
|
|
138
|
+
const files = await syncMethods.scanLocalFiles('/test-dir')
|
|
139
|
+
|
|
140
|
+
expect(fs.readdirSync).toHaveBeenCalledWith('/test-dir', {
|
|
141
|
+
withFileTypes: true,
|
|
142
|
+
})
|
|
143
|
+
// Updated assertion to match actual path format
|
|
144
|
+
expect(files).toEqual([
|
|
145
|
+
'//test.jpg',
|
|
146
|
+
'//images/photo.png',
|
|
147
|
+
'//images/subdir/deep.gif',
|
|
148
|
+
])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should throw error if directory not provided in Node environment', async () => {
|
|
152
|
+
// Save original window property
|
|
153
|
+
const originalWindow = global.window
|
|
154
|
+
|
|
155
|
+
// Delete window to simulate Node environment
|
|
156
|
+
delete (global as any).window
|
|
157
|
+
|
|
158
|
+
await expect(syncMethods.scanLocalFiles()).rejects.toThrow(
|
|
159
|
+
'Local directory must be provided in Node.js environment'
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Restore window property
|
|
163
|
+
global.window = originalWindow
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should handle errors when scanning directories', async () => {
|
|
167
|
+
;(fs.readdirSync as jest.Mock).mockImplementationOnce(() => {
|
|
168
|
+
throw new Error('Permission denied')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
172
|
+
|
|
173
|
+
const files = await syncMethods.scanLocalFiles('/test-dir')
|
|
174
|
+
|
|
175
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
176
|
+
expect(files).toEqual([])
|
|
177
|
+
|
|
178
|
+
consoleSpy.mockRestore()
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('scanDropboxFiles', () => {
|
|
183
|
+
it('should scan Dropbox folder and return image files', async () => {
|
|
184
|
+
const files = await syncMethods.scanDropboxFiles('/test-dir')
|
|
185
|
+
|
|
186
|
+
expect(mockDropboxClient.filesListFolder).toHaveBeenCalledWith({
|
|
187
|
+
path: '/test-dir',
|
|
188
|
+
recursive: true,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Updated assertion to match actual response structure
|
|
192
|
+
expect(files).toEqual(['/test.jpg', '/images/photo.png'])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should handle errors when scanning Dropbox', async () => {
|
|
196
|
+
mockDropboxClient.filesListFolder.mockRejectedValueOnce(
|
|
197
|
+
new Error('API error')
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
await expect(syncMethods.scanDropboxFiles()).rejects.toThrow(
|
|
201
|
+
'API error'
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should continue listing with cursor when has_more is true', async () => {
|
|
206
|
+
// Setup a response with has_more=true and a continuation response with additional files
|
|
207
|
+
mockDropboxClient.filesListFolder.mockResolvedValueOnce({
|
|
208
|
+
result: {
|
|
209
|
+
entries: [{ '.tag': 'file', path_display: '/first.jpg' }],
|
|
210
|
+
has_more: true,
|
|
211
|
+
cursor: 'test-cursor',
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
mockDropboxClient.filesListFolderContinue.mockResolvedValueOnce({
|
|
216
|
+
result: {
|
|
217
|
+
entries: [
|
|
218
|
+
{ '.tag': 'file', path_display: '/more/second.jpg' },
|
|
219
|
+
],
|
|
220
|
+
has_more: false,
|
|
221
|
+
cursor: 'test-cursor-2',
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const files = await syncMethods.scanDropboxFiles()
|
|
226
|
+
|
|
227
|
+
expect(
|
|
228
|
+
mockDropboxClient.filesListFolderContinue
|
|
229
|
+
).toHaveBeenCalledWith({
|
|
230
|
+
cursor: 'test-cursor',
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(files).toEqual(['/first.jpg', '/more/second.jpg'])
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('createSyncQueue', () => {
|
|
238
|
+
it('should create upload and download queues', async () => {
|
|
239
|
+
// Mock scan results
|
|
240
|
+
jest.spyOn(syncMethods, 'scanLocalFiles').mockResolvedValue([
|
|
241
|
+
'/local1.jpg',
|
|
242
|
+
'/local2.png',
|
|
243
|
+
])
|
|
244
|
+
|
|
245
|
+
jest.spyOn(syncMethods, 'scanDropboxFiles').mockResolvedValue([
|
|
246
|
+
'/dropbox1.jpg',
|
|
247
|
+
'/dropbox2.png',
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
const { uploadQueue, downloadQueue } =
|
|
251
|
+
await syncMethods.createSyncQueue({
|
|
252
|
+
localDir: '/local-dir',
|
|
253
|
+
dropboxDir: '/dropbox-dir',
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Files in local but not in Dropbox
|
|
257
|
+
expect(uploadQueue).toEqual(['/local1.jpg', '/local2.png'])
|
|
258
|
+
|
|
259
|
+
// Files in Dropbox but not in local
|
|
260
|
+
expect(downloadQueue).toEqual(['/dropbox1.jpg', '/dropbox2.png'])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should use default directories when none provided', async () => {
|
|
264
|
+
// Clear previous mocks to start fresh
|
|
265
|
+
jest.clearAllMocks()
|
|
266
|
+
|
|
267
|
+
// Mock both scan methods to return empty arrays to simplify the test
|
|
268
|
+
const scanLocalSpy = jest
|
|
269
|
+
.spyOn(syncMethods, 'scanLocalFiles')
|
|
270
|
+
.mockResolvedValue([])
|
|
271
|
+
const scanDropboxSpy = jest
|
|
272
|
+
.spyOn(syncMethods, 'scanDropboxFiles')
|
|
273
|
+
.mockResolvedValue([])
|
|
274
|
+
|
|
275
|
+
await syncMethods.createSyncQueue()
|
|
276
|
+
|
|
277
|
+
// Verify both scan methods were called
|
|
278
|
+
expect(scanLocalSpy).toHaveBeenCalled()
|
|
279
|
+
expect(scanDropboxSpy).toHaveBeenCalled()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should normalize paths for comparison', async () => {
|
|
283
|
+
// Clear previous mocks
|
|
284
|
+
jest.clearAllMocks()
|
|
285
|
+
|
|
286
|
+
// Different case and leading slashes but same file
|
|
287
|
+
jest.spyOn(syncMethods, 'scanLocalFiles').mockResolvedValue([
|
|
288
|
+
'/FILE.jpg',
|
|
289
|
+
])
|
|
290
|
+
jest.spyOn(syncMethods, 'scanDropboxFiles').mockResolvedValue([
|
|
291
|
+
'///file.jpg',
|
|
292
|
+
])
|
|
293
|
+
|
|
294
|
+
const { uploadQueue, downloadQueue } =
|
|
295
|
+
await syncMethods.createSyncQueue()
|
|
296
|
+
|
|
297
|
+
// Should not include files with normalized paths that match
|
|
298
|
+
expect(uploadQueue).toEqual([])
|
|
299
|
+
expect(downloadQueue).toEqual([])
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('syncFiles', () => {
|
|
304
|
+
beforeEach(() => {
|
|
305
|
+
// Mock createSyncQueue for sync tests
|
|
306
|
+
jest.spyOn(syncMethods, 'createSyncQueue').mockResolvedValue({
|
|
307
|
+
uploadQueue: ['/upload1.jpg', '/upload2.png'],
|
|
308
|
+
downloadQueue: ['/download1.jpg', '/download2.png'],
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('should upload and download files', async () => {
|
|
313
|
+
const result = await syncMethods.syncFiles({
|
|
314
|
+
localDir: '/local-dir',
|
|
315
|
+
dropboxDir: '/dropbox-dir',
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
// Check if progress was reported
|
|
319
|
+
expect(mockSocketMethods.emit).toHaveBeenCalledWith(
|
|
320
|
+
'sync:progress',
|
|
321
|
+
expect.anything()
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Check if files were uploaded
|
|
325
|
+
expect(mockDropboxClient.filesUpload).toHaveBeenCalledTimes(2)
|
|
326
|
+
expect(fs.readFileSync).toHaveBeenCalledTimes(2)
|
|
327
|
+
|
|
328
|
+
// Check if files were downloaded
|
|
329
|
+
expect(mockDropboxClient.filesDownload).toHaveBeenCalledTimes(2)
|
|
330
|
+
expect(fs.writeFileSync).toHaveBeenCalledTimes(2)
|
|
331
|
+
|
|
332
|
+
// Check if directories were created for downloaded files
|
|
333
|
+
expect(fs.existsSync).toHaveBeenCalledTimes(2)
|
|
334
|
+
expect(fs.mkdirSync).toHaveBeenCalledTimes(0) // Should be 0 since we mocked existsSync to return true
|
|
335
|
+
|
|
336
|
+
// Check the result
|
|
337
|
+
expect(result.uploaded).toEqual(['/upload1.jpg', '/upload2.png'])
|
|
338
|
+
expect(result.downloaded).toEqual([
|
|
339
|
+
'/download1.jpg',
|
|
340
|
+
'/download2.png',
|
|
341
|
+
])
|
|
342
|
+
expect(result.errors).toEqual([])
|
|
343
|
+
|
|
344
|
+
// Check that completion was emitted
|
|
345
|
+
expect(mockSocketMethods.emit).toHaveBeenCalledWith(
|
|
346
|
+
'sync:complete',
|
|
347
|
+
expect.anything()
|
|
348
|
+
)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('should handle upload errors', async () => {
|
|
352
|
+
// Mock one upload to fail
|
|
353
|
+
mockDropboxClient.filesUpload.mockRejectedValueOnce(
|
|
354
|
+
new Error('Upload failed')
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
358
|
+
|
|
359
|
+
const result = await syncMethods.syncFiles()
|
|
360
|
+
|
|
361
|
+
expect(result.errors.length).toBe(1)
|
|
362
|
+
expect(result.errors[0].file).toBe('/upload1.jpg')
|
|
363
|
+
expect(result.uploaded).toEqual(['/upload2.png'])
|
|
364
|
+
expect(result.downloaded).toEqual([
|
|
365
|
+
'/download1.jpg',
|
|
366
|
+
'/download2.png',
|
|
367
|
+
])
|
|
368
|
+
|
|
369
|
+
consoleSpy.mockRestore()
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('should handle download errors', async () => {
|
|
373
|
+
// Mock one download to fail
|
|
374
|
+
mockDropboxClient.filesDownload.mockRejectedValueOnce(
|
|
375
|
+
new Error('Download failed')
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
379
|
+
|
|
380
|
+
const result = await syncMethods.syncFiles()
|
|
381
|
+
|
|
382
|
+
expect(result.errors.length).toBe(1)
|
|
383
|
+
expect(result.errors[0].file).toBe('/download1.jpg')
|
|
384
|
+
expect(result.uploaded).toEqual(['/upload1.jpg', '/upload2.png'])
|
|
385
|
+
expect(result.downloaded).toEqual(['/download2.png'])
|
|
386
|
+
|
|
387
|
+
// Check that error was emitted
|
|
388
|
+
expect(mockSocketMethods.emit).toHaveBeenCalledWith(
|
|
389
|
+
'sync:error',
|
|
390
|
+
expect.objectContaining({
|
|
391
|
+
message: expect.stringContaining('Download failed'),
|
|
392
|
+
})
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
consoleSpy.mockRestore()
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it("should create directory if it doesn't exist", async () => {
|
|
399
|
+
// Mock directory not existing
|
|
400
|
+
;(fs.existsSync as jest.Mock).mockReturnValue(false)
|
|
401
|
+
|
|
402
|
+
await syncMethods.syncFiles()
|
|
403
|
+
|
|
404
|
+
expect(fs.mkdirSync).toHaveBeenCalledTimes(2)
|
|
405
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.anything(), {
|
|
406
|
+
recursive: true,
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should report when no files need syncing', async () => {
|
|
411
|
+
jest.spyOn(syncMethods, 'createSyncQueue').mockResolvedValue({
|
|
412
|
+
uploadQueue: [],
|
|
413
|
+
downloadQueue: [],
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
const result = await syncMethods.syncFiles()
|
|
417
|
+
|
|
418
|
+
expect(mockSocketMethods.emit).toHaveBeenCalledWith(
|
|
419
|
+
'sync:complete',
|
|
420
|
+
{
|
|
421
|
+
message: 'All files are already in sync',
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
expect(result.uploaded).toEqual([])
|
|
426
|
+
expect(result.downloaded).toEqual([])
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should handle main sync error', async () => {
|
|
430
|
+
jest.spyOn(syncMethods, 'createSyncQueue').mockRejectedValue(
|
|
431
|
+
new Error('Sync failed')
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
await expect(syncMethods.syncFiles()).rejects.toThrow('Sync failed')
|
|
435
|
+
|
|
436
|
+
expect(mockSocketMethods.emit).toHaveBeenCalledWith('sync:error', {
|
|
437
|
+
message: 'Sync failed',
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('should respect progress callback', async () => {
|
|
442
|
+
const mockProgressCallback = jest.fn()
|
|
443
|
+
|
|
444
|
+
await syncMethods.syncFiles({
|
|
445
|
+
progressCallback: mockProgressCallback,
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
expect(mockProgressCallback).toHaveBeenCalled()
|
|
449
|
+
expect(mockProgressCallback).toHaveBeenCalledWith(
|
|
450
|
+
expect.objectContaining({
|
|
451
|
+
stage: 'init',
|
|
452
|
+
progress: 0,
|
|
453
|
+
})
|
|
454
|
+
)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should handle file blob conversion', async () => {
|
|
458
|
+
// Clear previous mocks
|
|
459
|
+
jest.clearAllMocks()
|
|
460
|
+
|
|
461
|
+
// Mock the download response to return a Blob
|
|
462
|
+
const mockBlob = {
|
|
463
|
+
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(10)),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Create a simplified version of our test that focuses only on blob handling
|
|
467
|
+
jest.spyOn(syncMethods, 'createSyncQueue').mockResolvedValue({
|
|
468
|
+
uploadQueue: [],
|
|
469
|
+
downloadQueue: ['/download-blob.jpg'],
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// Setup a response with a fileBlob property
|
|
473
|
+
mockDropboxClient.filesDownload.mockResolvedValueOnce({
|
|
474
|
+
result: { fileBlob: mockBlob },
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
await syncMethods.syncFiles()
|
|
478
|
+
|
|
479
|
+
expect(mockBlob.arrayBuffer).toHaveBeenCalled()
|
|
480
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
481
|
+
expect.any(String),
|
|
482
|
+
expect.any(Buffer)
|
|
483
|
+
)
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
describe('cancelSync', () => {
|
|
488
|
+
it('should cancel ongoing sync', async () => {
|
|
489
|
+
// Start a long sync operation but don't await it
|
|
490
|
+
const syncPromise = syncMethods.syncFiles()
|
|
491
|
+
|
|
492
|
+
// Cancel the sync
|
|
493
|
+
syncMethods.cancelSync()
|
|
494
|
+
|
|
495
|
+
// Complete the sync and check result
|
|
496
|
+
const result = await syncPromise
|
|
497
|
+
|
|
498
|
+
expect(mockSocketMethods.emit).toHaveBeenCalledWith(
|
|
499
|
+
'sync:cancel',
|
|
500
|
+
{}
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
// Result should be empty since we cancelled before uploading/downloading
|
|
504
|
+
expect(result.uploaded).toEqual([])
|
|
505
|
+
expect(result.downloaded).toEqual([])
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
})
|