@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.
Files changed (32) hide show
  1. package/.github/workflows/test-pr.yml +30 -0
  2. package/README.md +207 -1
  3. package/__mocks__/nuxt/app.js +20 -0
  4. package/dist/adapters/__tests__/angular.spec.d.ts +1 -0
  5. package/dist/adapters/__tests__/angular.spec.js +237 -0
  6. package/dist/adapters/__tests__/next.spec.d.ts +1 -0
  7. package/dist/adapters/__tests__/next.spec.js +179 -0
  8. package/dist/adapters/__tests__/nuxt.spec.d.ts +1 -0
  9. package/dist/adapters/__tests__/nuxt.spec.js +145 -0
  10. package/dist/adapters/__tests__/svelte.spec.d.ts +1 -0
  11. package/dist/adapters/__tests__/svelte.spec.js +149 -0
  12. package/dist/core/__tests__/auth.spec.d.ts +1 -0
  13. package/dist/core/__tests__/auth.spec.js +83 -0
  14. package/dist/core/__tests__/client.spec.d.ts +1 -0
  15. package/dist/core/__tests__/client.spec.js +102 -0
  16. package/dist/core/__tests__/socket.spec.d.ts +1 -0
  17. package/dist/core/__tests__/socket.spec.js +122 -0
  18. package/dist/core/__tests__/sync.spec.d.ts +1 -0
  19. package/dist/core/__tests__/sync.spec.js +375 -0
  20. package/dist/core/sync.js +30 -11
  21. package/jest.config.js +24 -0
  22. package/jest.setup.js +38 -0
  23. package/package.json +4 -1
  24. package/src/adapters/__tests__/angular.spec.ts +338 -0
  25. package/src/adapters/__tests__/next.spec.ts +240 -0
  26. package/src/adapters/__tests__/nuxt.spec.ts +185 -0
  27. package/src/adapters/__tests__/svelte.spec.ts +194 -0
  28. package/src/core/__tests__/auth.spec.ts +142 -0
  29. package/src/core/__tests__/client.spec.ts +128 -0
  30. package/src/core/__tests__/socket.spec.ts +153 -0
  31. package/src/core/__tests__/sync.spec.ts +508 -0
  32. package/src/core/sync.ts +53 -26
@@ -0,0 +1,30 @@
1
+ name: Test Pull Request
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ node-version: [20.x]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+
20
+ - name: Use Node.js ${{ matrix.node-version }}
21
+ uses: actions/setup-node@v3
22
+ with:
23
+ node-version: ${{ matrix.node-version }}
24
+ cache: 'npm'
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Run tests
30
+ run: npm test
package/README.md CHANGED
@@ -1,9 +1,40 @@
1
1
  # Dropbox Sync Module
2
2
 
3
+ [![Test Pull Request](https://github.com/sweetenr/dropbox-sync-service/actions/workflows/test-pr.yml/badge.svg)](https://github.com/sweetenr/dropbox-sync-service/actions/workflows/test-pr.yml)
4
+
3
5
  A reusable TypeScript module for syncing files between your application and Dropbox. Features framework-specific adapters for Next.js, SvelteKit, Nuxt, and Angular.
4
6
 
5
7
  > N.B. This NPM package is neither tested nor supported
6
8
 
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Core Usage](#core-usage)
15
+ - [Development Setup](#development-setup)
16
+ - [Architecture](#architecture)
17
+ - [Data Flow](#data-flow)
18
+ - [Testing](#testing)
19
+ - [Test Structure](#test-structure)
20
+ - [Running Tests](#running-tests)
21
+ - [Testing Approach](#testing-approach)
22
+ - [Mock Strategy](#mock-strategy)
23
+ - [Test Coverage](#test-coverage)
24
+ - [Framework-Specific Usage](#framework-specific-usage)
25
+ - [Next.js](#nextjs)
26
+ - [Nuxt.js](#nuxtjs)
27
+ - [SvelteKit](#sveltekit)
28
+ - [Angular](#angular)
29
+ - [API Reference](#api-reference)
30
+ - [Core Client](#core-client)
31
+ - [Auth Methods](#auth-methods)
32
+ - [Sync Methods](#sync-methods)
33
+ - [Socket Methods](#socket-methods)
34
+ - [Socket Events](#socket-events)
35
+ - [Configuration Options](#configuration-options)
36
+ - [License](#license)
37
+
7
38
  ## Features
8
39
 
9
40
  - **File Synchronization**: Upload local files to Dropbox and download Dropbox files to your local filesystem
@@ -16,7 +47,18 @@ A reusable TypeScript module for syncing files between your application and Drop
16
47
  ## Installation
17
48
 
18
49
  ```bash
19
- npm install dropbox-sync socket.io socket.io-client
50
+ # Install the package in your project
51
+ npm install dropbox-sync
52
+ # or
53
+ yarn add dropbox-sync
54
+ ```
55
+
56
+ You may also need to install peer dependencies:
57
+
58
+ ```bash
59
+ npm install socket.io socket.io-client
60
+ # or
61
+ yarn add socket.io socket.io-client
20
62
  ```
21
63
 
22
64
  ## Quick Start
@@ -54,6 +96,170 @@ console.log(`Uploaded ${syncResult.uploaded.length} files`)
54
96
  console.log(`Downloaded ${syncResult.downloaded.length} files`)
55
97
  ```
56
98
 
99
+ ## Development Setup
100
+
101
+ To set up this project for local development:
102
+
103
+ ```bash
104
+ # Clone the repository
105
+ git clone https://github.com/yourusername/dropbox-sync-service.git
106
+ cd dropbox-sync-service
107
+
108
+ # Install dependencies
109
+ npm install
110
+
111
+ # Run tests
112
+ npm test
113
+
114
+ # Build the package
115
+ npm run build
116
+ ```
117
+
118
+ ## Architecture
119
+
120
+ The Dropbox Sync Module architecture is designed with modularity, extensibility, and framework-agnosticism in mind.
121
+
122
+ ```mermaid
123
+ graph TD
124
+ A[Application] --> B[Framework Adapter]
125
+ B --> C[Core Client]
126
+ C --> D[Auth Module]
127
+ C --> E[Sync Module]
128
+ C --> F[Socket Module]
129
+ D --> G[Dropbox API]
130
+ E --> G
131
+ F --> H[Socket.IO]
132
+ H --> E
133
+ H --> A
134
+
135
+ %% File Flow
136
+ I[Local Files] --> E
137
+ E --> I
138
+ G --> E
139
+ E --> G
140
+ ```
141
+
142
+ ### Data Flow
143
+
144
+ 1. **Authentication Flow:**
145
+
146
+ ```mermaid
147
+ sequenceDiagram
148
+ participant User
149
+ participant App
150
+ participant DropboxSync
151
+ participant Dropbox
152
+
153
+ User->>App: Initiate auth
154
+ App->>DropboxSync: getAuthUrl()
155
+ DropboxSync->>Dropbox: Request auth URL
156
+ Dropbox-->>DropboxSync: Auth URL
157
+ DropboxSync-->>App: Auth URL
158
+ App-->>User: Redirect to auth URL
159
+ User->>Dropbox: Authorize app
160
+ Dropbox-->>App: Auth code (via redirect)
161
+ App->>DropboxSync: exchangeCodeForToken(code)
162
+ DropboxSync->>Dropbox: Exchange code for token
163
+ Dropbox-->>DropboxSync: Access & refresh tokens
164
+ DropboxSync-->>App: Tokens
165
+ ```
166
+
167
+ 2. **Sync Flow:**
168
+
169
+ ```mermaid
170
+ sequenceDiagram
171
+ participant App
172
+ participant DropboxSync
173
+ participant Socket
174
+ participant Dropbox
175
+ participant LocalFS
176
+
177
+ App->>DropboxSync: syncFiles(options)
178
+ DropboxSync->>LocalFS: Scan local files
179
+ LocalFS-->>DropboxSync: Local file list
180
+ DropboxSync->>Dropbox: List files
181
+ Dropbox-->>DropboxSync: Dropbox file list
182
+ DropboxSync->>DropboxSync: Create sync queue
183
+
184
+ loop For each file to upload
185
+ DropboxSync->>Dropbox: Upload file
186
+ DropboxSync->>Socket: Emit progress
187
+ Socket-->>App: Progress update
188
+ end
189
+
190
+ loop For each file to download
191
+ DropboxSync->>Dropbox: Download file
192
+ DropboxSync->>LocalFS: Write file
193
+ DropboxSync->>Socket: Emit progress
194
+ Socket-->>App: Progress update
195
+ end
196
+
197
+ DropboxSync-->>App: Sync results
198
+ ```
199
+
200
+ ## Testing
201
+
202
+ The Dropbox Sync Module uses Jest for unit testing. Tests are organized to match the structure of the source code.
203
+
204
+ ### Test Structure
205
+
206
+ ```
207
+ __tests__/ # Test utilities and mocks
208
+ src/
209
+ core/
210
+ __tests__/ # Core module tests
211
+ auth.spec.ts # Authentication tests
212
+ client.spec.ts # Client tests
213
+ socket.spec.ts # Socket tests
214
+ sync.spec.ts # Sync tests
215
+ adapters/
216
+ __tests__/ # Framework adapter tests
217
+ angular.spec.ts # Angular adapter tests
218
+ next.spec.ts # Next.js adapter tests
219
+ nuxt.spec.ts # Nuxt adapter tests
220
+ svelte.spec.ts # SvelteKit adapter tests
221
+ ```
222
+
223
+ ### Running Tests
224
+
225
+ ```bash
226
+ # Run all tests
227
+ npm test
228
+
229
+ # Run tests with coverage
230
+ npm test -- --coverage
231
+
232
+ # Run specific test file
233
+ npm test -- src/core/__tests__/auth.spec.ts
234
+
235
+ # Run tests in watch mode during development
236
+ npm test -- --watch
237
+ ```
238
+
239
+ ### Testing Approach
240
+
241
+ 1. **Unit Tests**: Each module is tested in isolation with mocked dependencies
242
+ 2. **Integration Tests**: Tests for interactions between modules
243
+ 3. **Adapter Tests**: Tests for framework-specific adapters
244
+
245
+ ### Mock Strategy
246
+
247
+ - Dropbox API calls are mocked using Jest mock functions
248
+ - File system operations are mocked to avoid touching real files
249
+ - Socket.IO is mocked to test event emission and handling
250
+
251
+ ### Test Coverage
252
+
253
+ We aim for high test coverage, especially for the core functionality:
254
+
255
+ | Module | Coverage |
256
+ | -------- | -------- |
257
+ | Core | >90% |
258
+ | Auth | >95% |
259
+ | Sync | >90% |
260
+ | Socket | >90% |
261
+ | Adapters | >85% |
262
+
57
263
  ## Framework-Specific Usage
58
264
 
59
265
  ### Next.js
@@ -0,0 +1,20 @@
1
+ // Manual mock for nuxt/app module
2
+ module.exports = {
3
+ useRuntimeConfig: jest.fn().mockReturnValue({
4
+ public: {
5
+ dropboxAppKey: 'test-app-key',
6
+ appUrl: 'https://example.com'
7
+ },
8
+ dropboxAppSecret: 'test-app-secret',
9
+ dropboxRedirectUri: 'https://example.com/api/dropbox/auth/callback'
10
+ }),
11
+ useCookie: jest.fn().mockImplementation((name) => {
12
+ if (name === 'dropbox_access_token') {
13
+ return { value: 'test-access-token' }
14
+ }
15
+ if (name === 'dropbox_refresh_token') {
16
+ return { value: 'test-refresh-token' }
17
+ }
18
+ return { value: null }
19
+ })
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,237 @@
1
+ // Mock Angular modules before importing the code under test
2
+ jest.mock('@angular/core', () => ({
3
+ Injectable: () => jest.fn(),
4
+ }));
5
+ jest.mock('@angular/common/http', () => ({
6
+ HttpClient: jest.fn(),
7
+ HttpHeaders: jest.fn(),
8
+ }));
9
+ // Now import the code that uses Angular modules
10
+ import { DropboxSyncService, getCredentialsFromEnvironment } from '../angular';
11
+ import { createDropboxSyncClient } from '../../core/client';
12
+ import { of } from 'rxjs';
13
+ import { Observable } from 'rxjs';
14
+ // Mock the core client
15
+ jest.mock('../../core/client');
16
+ describe('Angular adapter', () => {
17
+ let service;
18
+ const mockHttpClient = {
19
+ get: jest.fn(),
20
+ post: jest.fn(),
21
+ };
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ service = new DropboxSyncService(mockHttpClient);
25
+ });
26
+ describe('DropboxSyncService', () => {
27
+ it('should be created', () => {
28
+ expect(service).toBeTruthy();
29
+ });
30
+ it('should initialize with credentials', () => {
31
+ const mockCredentials = {
32
+ clientId: 'test-client-id',
33
+ clientSecret: 'test-client-secret',
34
+ accessToken: 'test-access-token',
35
+ refreshToken: 'test-refresh-token',
36
+ };
37
+ const mockClient = { mock: 'client' };
38
+ createDropboxSyncClient.mockReturnValue(mockClient);
39
+ const result = service.initialize(mockCredentials);
40
+ expect(createDropboxSyncClient).toHaveBeenCalledWith(mockCredentials);
41
+ expect(result).toEqual(mockClient);
42
+ });
43
+ it('should get client after initialization', () => {
44
+ const mockCredentials = { clientId: 'test-id' };
45
+ const mockClient = { mock: 'client' };
46
+ createDropboxSyncClient.mockReturnValue(mockClient);
47
+ service.initialize(mockCredentials);
48
+ const client = service.getClient();
49
+ expect(client).toEqual(mockClient);
50
+ });
51
+ it('should throw error if getClient called before initialization', () => {
52
+ expect(() => service.getClient()).toThrow('DropboxSyncService not initialized. Call initialize() first.');
53
+ });
54
+ it('should check connection status via HTTP', () => {
55
+ mockHttpClient.get.mockReturnValue(of({ connected: true }));
56
+ service.checkConnection().subscribe((result) => {
57
+ expect(result).toBe(true);
58
+ });
59
+ expect(mockHttpClient.get).toHaveBeenCalledWith('/api/dropbox/status');
60
+ });
61
+ it('should handle connection error', () => {
62
+ mockHttpClient.get.mockReturnValue(of(null)); // Simulate error
63
+ service.checkConnection().subscribe((result) => {
64
+ expect(result).toBe(false);
65
+ });
66
+ });
67
+ it('should disconnect via HTTP', () => {
68
+ mockHttpClient.post.mockReturnValue(of({ success: true }));
69
+ service.disconnectDropbox().subscribe((result) => {
70
+ expect(result).toBe(true);
71
+ });
72
+ expect(mockHttpClient.post).toHaveBeenCalledWith('/api/dropbox/logout', {});
73
+ });
74
+ it('should start sync process', () => {
75
+ const mockOptions = { localDir: '/test-dir' };
76
+ const mockResult = { uploaded: [], downloaded: [] };
77
+ const mockClient = {
78
+ sync: {
79
+ syncFiles: jest.fn().mockResolvedValue(mockResult),
80
+ },
81
+ socket: {
82
+ connect: jest.fn(),
83
+ },
84
+ };
85
+ createDropboxSyncClient.mockReturnValue(mockClient);
86
+ service.initialize({ clientId: 'test-id' });
87
+ // Mock setupSocketListeners to return an observable
88
+ service.setupSocketListeners = jest.fn().mockReturnValue(of({}));
89
+ service.startSync(mockOptions).subscribe((result) => {
90
+ expect(result).toEqual(mockResult);
91
+ });
92
+ expect(mockClient.socket.connect).toHaveBeenCalled();
93
+ expect(mockClient.sync.syncFiles).toHaveBeenCalledWith(mockOptions);
94
+ expect(service.setupSocketListeners).toHaveBeenCalled();
95
+ });
96
+ it('should cancel sync', () => {
97
+ const mockClient = {
98
+ sync: {
99
+ cancelSync: jest.fn(),
100
+ },
101
+ };
102
+ createDropboxSyncClient.mockReturnValue(mockClient);
103
+ service.initialize({ clientId: 'test-id' });
104
+ service.cancelSync();
105
+ expect(mockClient.sync.cancelSync).toHaveBeenCalled();
106
+ });
107
+ // Mock the implementation of setupSocketListeners for testing
108
+ it('should setup socket listeners', () => {
109
+ const mockSocket = {
110
+ on: jest.fn(),
111
+ off: jest.fn(),
112
+ };
113
+ const mockClient = {
114
+ socket: mockSocket,
115
+ };
116
+ createDropboxSyncClient.mockReturnValue(mockClient);
117
+ service.initialize({ clientId: 'test-id' });
118
+ const observable = service.setupSocketListeners();
119
+ // Should return an Observable
120
+ expect(observable).toBeInstanceOf(Observable);
121
+ // Get the subscriber function that was used to create the Observable
122
+ const observer = {
123
+ next: jest.fn(),
124
+ error: jest.fn(),
125
+ complete: jest.fn(),
126
+ };
127
+ const subscription = observable.subscribe(observer);
128
+ // Since we have direct access to the socket mock, we can verify its behavior
129
+ expect(mockSocket.on).toHaveBeenCalledWith('sync:progress', expect.any(Function));
130
+ expect(mockSocket.on).toHaveBeenCalledWith('sync:queue', expect.any(Function));
131
+ expect(mockSocket.on).toHaveBeenCalledWith('sync:complete', expect.any(Function));
132
+ expect(mockSocket.on).toHaveBeenCalledWith('sync:error', expect.any(Function));
133
+ // Clean up the subscription
134
+ subscription.unsubscribe();
135
+ });
136
+ it('should handle OAuth callback', () => {
137
+ const mockCode = 'test-auth-code';
138
+ const mockTokens = {
139
+ accessToken: 'new-access-token',
140
+ refreshToken: 'new-refresh-token',
141
+ };
142
+ const mockClient = {
143
+ auth: {
144
+ exchangeCodeForToken: jest
145
+ .fn()
146
+ .mockResolvedValue(mockTokens),
147
+ },
148
+ };
149
+ createDropboxSyncClient.mockReturnValue(mockClient);
150
+ service.initialize({ clientId: 'test-id' });
151
+ // Create a proper localStorage mock
152
+ const mockLocalStorage = {
153
+ setItem: jest.fn(),
154
+ getItem: jest.fn(),
155
+ removeItem: jest.fn(),
156
+ };
157
+ Object.defineProperty(global, 'localStorage', {
158
+ value: mockLocalStorage,
159
+ writable: true,
160
+ });
161
+ // Mock window.location.origin
162
+ Object.defineProperty(window, 'location', {
163
+ value: {
164
+ origin: 'http://localhost',
165
+ },
166
+ writable: true,
167
+ });
168
+ // Subscribe to the observable to trigger the code
169
+ let receivedResult;
170
+ service.handleOAuthCallback(mockCode).subscribe((result) => {
171
+ receivedResult = result;
172
+ });
173
+ // Verify the exchangeCodeForToken was called with the expected args
174
+ expect(mockClient.auth.exchangeCodeForToken).toHaveBeenCalledWith(mockCode, 'http://localhost/api/dropbox/auth/callback');
175
+ // Create and resolve the promise to trigger the localStorage set calls
176
+ const resolvePromise = Promise.resolve(mockTokens);
177
+ // Return a promise that will resolve after our mocked promise
178
+ return resolvePromise.then(() => {
179
+ // Now we can check localStorage was called
180
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('dropbox_access_token', mockTokens.accessToken);
181
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('dropbox_refresh_token', mockTokens.refreshToken);
182
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('dropbox_connected', 'true');
183
+ // Verify we received the tokens
184
+ expect(receivedResult).toEqual(mockTokens);
185
+ });
186
+ });
187
+ });
188
+ describe('getCredentialsFromEnvironment', () => {
189
+ it('should extract credentials from environment', () => {
190
+ const mockEnvironment = {
191
+ dropboxAppKey: 'env-app-key',
192
+ dropboxAppSecret: 'env-app-secret',
193
+ };
194
+ // Mock localStorage
195
+ const mockLocalStorage = {
196
+ getItem: jest.fn().mockImplementation((key) => {
197
+ if (key === 'dropbox_access_token')
198
+ return 'storage-access-token';
199
+ if (key === 'dropbox_refresh_token')
200
+ return 'storage-refresh-token';
201
+ return null;
202
+ }),
203
+ };
204
+ Object.defineProperty(global, 'localStorage', {
205
+ value: mockLocalStorage,
206
+ writable: true,
207
+ });
208
+ const credentials = getCredentialsFromEnvironment(mockEnvironment);
209
+ expect(credentials).toEqual({
210
+ clientId: 'env-app-key',
211
+ clientSecret: 'env-app-secret',
212
+ accessToken: 'storage-access-token',
213
+ refreshToken: 'storage-refresh-token',
214
+ });
215
+ expect(mockLocalStorage.getItem).toHaveBeenCalledWith('dropbox_access_token');
216
+ expect(mockLocalStorage.getItem).toHaveBeenCalledWith('dropbox_refresh_token');
217
+ });
218
+ it('should handle missing environment values', () => {
219
+ const mockEnvironment = {};
220
+ // Mock localStorage with no tokens
221
+ const mockLocalStorage = {
222
+ getItem: jest.fn().mockReturnValue(null),
223
+ };
224
+ Object.defineProperty(global, 'localStorage', {
225
+ value: mockLocalStorage,
226
+ writable: true,
227
+ });
228
+ const credentials = getCredentialsFromEnvironment(mockEnvironment);
229
+ expect(credentials).toEqual({
230
+ clientId: '',
231
+ clientSecret: undefined,
232
+ accessToken: undefined,
233
+ refreshToken: undefined,
234
+ });
235
+ });
236
+ });
237
+ });
@@ -0,0 +1 @@
1
+ export {};