google-drive-mock 1.1.6 → 1.2.0

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 (48) hide show
  1. package/AGENTS.md +4 -1
  2. package/CLAUDE.md +17 -0
  3. package/dist/index.js +2 -1
  4. package/dist/mappers.d.ts +5 -0
  5. package/dist/mappers.js +15 -0
  6. package/dist/routes/v2.js +16 -8
  7. package/dist/routes/v3.js +30 -9
  8. package/dist/store.js +2 -2
  9. package/package.json +4 -4
  10. package/scripts/check-token.ts +107 -38
  11. package/scripts/run-loop.sh +18 -0
  12. package/src/index.ts +2 -1
  13. package/src/mappers.ts +15 -0
  14. package/src/routes/v2.ts +16 -8
  15. package/src/routes/v3.ts +35 -10
  16. package/src/store.ts +2 -2
  17. package/test/advanced_changes.test.ts +76 -29
  18. package/test/advanced_ordering.test.ts +2 -1
  19. package/test/basics.test.ts +34 -58
  20. package/test/batch_and_query.test.ts +28 -62
  21. package/test/batch_insert_download.test.ts +2 -1
  22. package/test/check_empty.test.ts +60 -0
  23. package/test/complex_query.test.ts +2 -1
  24. package/test/concurrent_fetch.test.ts +2 -1
  25. package/test/config.ts +164 -7
  26. package/test/dates_and_sorting.test.ts +2 -1
  27. package/test/etag.test.ts +8 -4
  28. package/test/features.test.ts +2 -1
  29. package/test/folder_query.test.ts +2 -1
  30. package/test/folder_search.test.ts +2 -1
  31. package/test/iterate_changes.test.ts +153 -68
  32. package/test/latency.test.ts +2 -1
  33. package/test/mime_types.test.ts +2 -1
  34. package/test/missing_fields.test.ts +2 -1
  35. package/test/multipart_behavior.test.ts +2 -1
  36. package/test/parallel_update.test.ts +2 -1
  37. package/test/parity_media_download.test.ts +2 -1
  38. package/test/routines.test.ts +15 -12
  39. package/test/upload.test.ts +2 -1
  40. package/test/url_parameters.test.ts +2 -1
  41. package/test/v2_basics.test.ts +22 -13
  42. package/test/v2_content.test.ts +2 -1
  43. package/test/v2_missing_ops.test.ts +75 -75
  44. package/test/v2_routes.test.ts +31 -21
  45. package/test/v2_upload.test.ts +17 -9
  46. package/test/v3_parity.test.ts +56 -20
  47. package/test_etag_headers.ts +92 -0
  48. package/vitest.config.ts +7 -1
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { getTestConfig, TestConfig } from './config';
3
+
4
+ describe('Check Mock Drive Empty', () => {
5
+ let config: TestConfig;
6
+
7
+ beforeAll(async () => {
8
+ config = await getTestConfig();
9
+ });
10
+
11
+ afterAll(() => {
12
+ if (config) config.stop();
13
+ });
14
+
15
+ it('should be empty (no files left except possibly google-drive-mock root)', async () => {
16
+ const headers = { 'Authorization': `Bearer ${config.token}` };
17
+ const res = await fetch(`${config.baseUrl}/drive/v3/files?supportsAllDrives=true&includeItemsFromAllDrives=true`, { headers });
18
+ expect(res.status).toBe(200);
19
+ const data = await res.json();
20
+
21
+ const files = data.files || [];
22
+
23
+ if (!config.baseUrl.includes('googleapis')) {
24
+ const nonRootFiles = files.filter((f: { name: string; id: string }) => f.name !== 'google-drive-mock');
25
+ if (nonRootFiles.length > 0) {
26
+ console.error('LOCKED/LEAKED FILES FOUND:', JSON.stringify(nonRootFiles, null, 2));
27
+ expect(nonRootFiles.length).toBe(0);
28
+ }
29
+ // Clean up the google-drive-mock folder if it exists
30
+ const rootFolder = files.find((f: { name: string; id: string }) => f.name === 'google-drive-mock');
31
+ if (rootFolder) {
32
+ const delRes = await fetch(`${config.baseUrl}/drive/v3/files/${rootFolder.id}`, {
33
+ method: 'DELETE',
34
+ headers
35
+ });
36
+ expect([204, 404]).toContain(delRes.status);
37
+
38
+ // Verify absolutely no files exist now
39
+ const checkRes = await fetch(`${config.baseUrl}/drive/v3/files?supportsAllDrives=true&includeItemsFromAllDrives=true`, { headers });
40
+ expect(checkRes.status).toBe(200);
41
+ const checkData = await checkRes.json();
42
+ expect(checkData.files || []).toEqual([]);
43
+ }
44
+ } else {
45
+ const q = `'${config.testFolderId}' in parents and trashed = false`;
46
+ const childrenRes = await fetch(`${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&supportsAllDrives=true&includeItemsFromAllDrives=true`, { headers });
47
+ expect(childrenRes.status).toBe(200);
48
+ const childrenData = await childrenRes.json();
49
+ const children = childrenData.files || [];
50
+ expect(children.length).toBe(0);
51
+
52
+ // Delete the test folder itself on the real API
53
+ const delRes = await fetch(`${config.baseUrl}/drive/v3/files/${config.testFolderId}`, {
54
+ method: 'DELETE',
55
+ headers
56
+ });
57
+ expect([204, 404]).toContain(delRes.status);
58
+ }
59
+ });
60
+ });
@@ -1,5 +1,6 @@
1
1
 
2
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { describe, expect, beforeAll, afterAll } from 'vitest';
3
+ import { it } from './config';;
3
4
  import { getTestConfig, TestConfig } from './config';
4
5
 
5
6
  describe('Complex Query Support', () => {
@@ -1,5 +1,6 @@
1
1
 
2
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { describe, expect, beforeAll, afterAll } from 'vitest';
3
+ import { it } from './config';;
3
4
  import { getTestConfig, TestConfig } from './config';
4
5
 
5
6
  // --- User's Logic (Batch Fetch) ---
package/test/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Note: We avoid static imports of node-only modules to support browser mode.
2
2
  // Types are fine.
3
3
  import type { Server } from 'http';
4
+ import { it as vitestIt, test as vitestTest, afterAll } from 'vitest';
4
5
 
5
6
 
6
7
  /**
@@ -14,6 +15,9 @@ export interface TestConfig {
14
15
  testFolderId: string;
15
16
  stop: () => void;
16
17
  clear: () => Promise<void>;
18
+ createdFiles: string[];
19
+ trackFile: (id: string | undefined | null) => void;
20
+ cleanup: () => Promise<void>;
17
21
  }
18
22
 
19
23
  async function ensureTestFolder(target: string, token: string, folderName: string): Promise<string> {
@@ -69,6 +73,88 @@ async function ensureTestFolder(target: string, token: string, folderName: strin
69
73
  return created.id;
70
74
  }
71
75
 
76
+ let activeConfig: TestConfig | undefined;
77
+
78
+ function wrapTestConfig(config: {
79
+ target: Server | string;
80
+ baseUrl: string;
81
+ token: string;
82
+ testFolderId: string;
83
+ stop: () => void;
84
+ clear: () => Promise<void>;
85
+ }): TestConfig {
86
+ const createdFiles: string[] = [];
87
+ const configWithTracking: TestConfig = {
88
+ ...config,
89
+ createdFiles,
90
+ trackFile: (id) => {
91
+ if (id && typeof id === 'string' && !createdFiles.includes(id) && id !== config.testFolderId) {
92
+ createdFiles.push(id);
93
+ }
94
+ },
95
+ cleanup: async () => {
96
+ const headers = { 'Authorization': `Bearer ${config.token}` };
97
+ // Delete in reverse order of creation (children before parents)
98
+ const idsToDelete = [...createdFiles].reverse();
99
+ for (const id of idsToDelete) {
100
+ try {
101
+ await fetch(`${config.baseUrl}/drive/v3/files/${id}`, {
102
+ method: 'DELETE',
103
+ headers
104
+ });
105
+ } catch {
106
+ // ignore network error
107
+ }
108
+ }
109
+ createdFiles.length = 0; // empty the list
110
+ }
111
+ };
112
+
113
+ // Patch globalThis.fetch to automatically track created files
114
+ const originalFetch = globalThis.fetch;
115
+ globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
116
+ const res = await originalFetch(input, init);
117
+ const method = init?.method?.toUpperCase() || 'GET';
118
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
119
+ try {
120
+ const urlStr = typeof input === 'string' ? input : (input instanceof URL ? input.toString() : input.url);
121
+ if (urlStr.includes('/files') || urlStr.includes('/upload') || urlStr.includes('/batch')) {
122
+ const clone = res.clone();
123
+ const contentType = clone.headers.get('content-type') || '';
124
+ if (contentType.includes('application/json')) {
125
+ const body = await clone.json();
126
+ if (body && typeof body === 'object') {
127
+ if (body.id && (body.kind === 'drive#file' || !body.kind)) {
128
+ configWithTracking.trackFile(body.id);
129
+ }
130
+ if (body.files && Array.isArray(body.files)) {
131
+ for (const f of body.files) {
132
+ if (f.id) configWithTracking.trackFile(f.id);
133
+ }
134
+ }
135
+ }
136
+ } else {
137
+ const text = await clone.text();
138
+ const matches = text.match(/"id"\s*:\s*"([^"]+)"/g);
139
+ if (matches) {
140
+ for (const match of matches) {
141
+ const id = match.replace(/"id"\s*:\s*"/, '').replace(/"/, '');
142
+ configWithTracking.trackFile(id);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ } catch {
148
+ // ignore tracking errors
149
+ }
150
+ }
151
+ return res;
152
+ };
153
+
154
+ activeConfig = configWithTracking;
155
+ return configWithTracking;
156
+ }
157
+
72
158
  export async function getTestConfig(): Promise<TestConfig> {
73
159
  const isBrowser = typeof window !== 'undefined';
74
160
  // For browser compatibility, we can't access process.env.TEST_TARGET easily
@@ -129,25 +215,26 @@ export async function getTestConfig(): Promise<TestConfig> {
129
215
  // Ensure scope folder
130
216
  const testFolderId = await ensureTestFolder(target, token, 'google-drive-mock');
131
217
 
132
- return {
218
+ return wrapTestConfig({
133
219
  target,
134
220
  baseUrl: target,
135
221
  token,
136
222
  testFolderId,
137
223
  stop: () => { },
138
224
  clear: async () => { }
139
- };
225
+ });
140
226
  }
141
227
 
142
228
  if (isBrowser) {
143
229
  console.log('Running tests against MOCK Google Drive API (Browser)');
144
- const serverUrl = 'http://localhost:3000';
230
+ const port = process.env.PORT || '3000';
231
+ const serverUrl = `http://localhost:${port}`;
145
232
  // In Mock mode, we can just use a random folder ID or create one if Mock supports it.
146
233
  // Mock supports folders. Let's create one to be safe and rigorous.
147
234
  const token = 'valid-token';
148
235
  const testFolderId = await ensureTestFolder(serverUrl, token, 'google-drive-mock');
149
236
 
150
- return {
237
+ return wrapTestConfig({
151
238
  target: serverUrl,
152
239
  baseUrl: serverUrl,
153
240
  token,
@@ -158,8 +245,28 @@ export async function getTestConfig(): Promise<TestConfig> {
158
245
  // We re-create the folder after clear in store or ensure checking logic handles it.
159
246
  await ensureTestFolder(serverUrl, token, 'google-drive-mock');
160
247
  }
161
- };
248
+ });
162
249
  } else {
250
+ const useSharedMock = process.env.USE_SHARED_MOCK === 'true';
251
+ if (useSharedMock) {
252
+ console.log('Running tests against MOCK Google Drive API (Shared Node)');
253
+ const port = process.env.PORT || '3000';
254
+ const targetUrl = `http://localhost:${port}`;
255
+ const token = 'valid-token';
256
+ const testFolderId = await ensureTestFolder(targetUrl, token, 'google-drive-mock');
257
+
258
+ return wrapTestConfig({
259
+ target: targetUrl,
260
+ baseUrl: targetUrl,
261
+ token,
262
+ testFolderId,
263
+ stop: () => { },
264
+ clear: async () => {
265
+ // Do not clear in parallel tests to avoid state corruption
266
+ }
267
+ });
268
+ }
269
+
163
270
  console.log('Running tests against MOCK Google Drive API (Node)');
164
271
  const { startServer } = await import('../src/index');
165
272
  const { driveStore } = await import('../src/store');
@@ -179,7 +286,7 @@ export async function getTestConfig(): Promise<TestConfig> {
179
286
  // Create Folder in Mock
180
287
  const testFolderId = await ensureTestFolder(targetUrl, 'valid-token', 'google-drive-mock');
181
288
 
182
- return {
289
+ return wrapTestConfig({
183
290
  target: server,
184
291
  baseUrl: targetUrl, // Added
185
292
  token: 'valid-token',
@@ -193,6 +300,56 @@ export async function getTestConfig(): Promise<TestConfig> {
193
300
  // We must re-create the folder after clear
194
301
  await ensureTestFolder(targetUrl, 'valid-token', 'google-drive-mock');
195
302
  }
196
- };
303
+ });
197
304
  }
198
305
  }
306
+
307
+ export async function cleanupCreatedFiles() {
308
+ if (activeConfig) {
309
+ await activeConfig.cleanup();
310
+ }
311
+ }
312
+
313
+ /* eslint-disable @typescript-eslint/no-explicit-any */
314
+ function wrapTestFunction(vitestFn: any): any {
315
+ const proxy = new Proxy(vitestFn, {
316
+ apply(target, thisArg, argArray) {
317
+ const [name, fn, timeout] = argArray;
318
+ if (typeof fn === 'function') {
319
+ return target.call(thisArg, name, async (...args: any[]) => {
320
+ try {
321
+ return await fn(...args);
322
+ } finally {
323
+ await cleanupCreatedFiles();
324
+ }
325
+ }, timeout);
326
+ }
327
+ return target.apply(thisArg, argArray as any);
328
+ },
329
+ get(target, prop, receiver) {
330
+ const val = Reflect.get(target, prop, receiver);
331
+ if (typeof val === 'function') {
332
+ return wrapTestFunction(val);
333
+ }
334
+ return val;
335
+ }
336
+ });
337
+ return proxy;
338
+ }
339
+ /* eslint-enable @typescript-eslint/no-explicit-any */
340
+
341
+ export const it = wrapTestFunction(vitestIt) as typeof vitestIt;
342
+ export const test = wrapTestFunction(vitestTest) as typeof vitestTest;
343
+
344
+ afterAll(async () => {
345
+ if (activeConfig && activeConfig.testFolderId) {
346
+ try {
347
+ await fetch(`${activeConfig.baseUrl}/drive/v3/files/${activeConfig.testFolderId}`, {
348
+ method: 'DELETE',
349
+ headers: { 'Authorization': `Bearer ${activeConfig.token}` }
350
+ });
351
+ } catch {
352
+ // ignore cleanup error
353
+ }
354
+ }
355
+ });
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1
+ import { describe, expect, beforeAll, afterAll } from 'vitest';
2
+ import { it } from './config';;
2
3
  import { getTestConfig, TestConfig } from './config';
3
4
 
4
5
  describe('Date Updates and Sorting', () => {
package/test/etag.test.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1
+ import { describe, expect, beforeAll, afterAll } from 'vitest';
2
+ import { it } from './config';;
2
3
  import { getTestConfig, TestConfig } from './config';
3
4
  import { Server } from 'http';
4
5
 
@@ -63,9 +64,10 @@ describe('ETag and If-Match Support', () => {
63
64
  }
64
65
 
65
66
  it('should return ETag header when getting a file (V2 Parity)', async () => {
67
+ const title = `ETag Test File V2 ${Math.random().toString(36).substring(7)}`;
66
68
  // Create file using V2 endpoint
67
69
  const createRes = await req('POST', '/drive/v2/files', {
68
- title: 'ETag Test File V2', // V2 uses 'title', not 'name'
70
+ title, // V2 uses 'title', not 'name'
69
71
  mimeType: 'text/plain',
70
72
  parents: [{ id: config.testFolderId }] // V2 parents is a list of ParentReferences
71
73
  });
@@ -85,9 +87,10 @@ describe('ETag and If-Match Support', () => {
85
87
  });
86
88
 
87
89
  it('should support If-Match on UPDATE (V2 Parity)', async () => {
90
+ const title = `If-Match V2 File ${Math.random().toString(36).substring(7)}`;
88
91
  // Create file
89
92
  const createRes = await req('POST', '/drive/v2/files', {
90
- title: 'If-Match V2 File',
93
+ title,
91
94
  parents: [{ id: config.testFolderId }]
92
95
  });
93
96
  const fileId = createRes.body.id;
@@ -129,8 +132,9 @@ describe('ETag and If-Match Support', () => {
129
132
  });
130
133
 
131
134
  it('should support If-Match on DELETE (V2 Parity)', async () => {
135
+ const title = `Delete V2 File ${Math.random().toString(36).substring(7)}`;
132
136
  const createRes = await req('POST', '/drive/v2/files', {
133
- title: 'Delete V2 File',
137
+ title,
134
138
  parents: [{ id: config.testFolderId }]
135
139
  });
136
140
  const fileId = createRes.body.id;
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1
+ import { describe, expect, beforeAll, afterAll } from 'vitest';
2
+ import { it } from './config';;
2
3
  import { getTestConfig, TestConfig } from './config';
3
4
  import { Server } from 'http';
4
5
  import { DriveFile } from '../src/store';
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll } from 'vitest';
1
+ import { describe, expect, beforeAll } from 'vitest';
2
+ import { it } from './config';;
2
3
  import { getTestConfig, TestConfig } from './config';
3
4
 
4
5
  const randomString = () => Math.random().toString(36).substring(7);
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll } from 'vitest';
1
+ import { describe, expect, beforeAll } from 'vitest';
2
+ import { it } from './config';;
2
3
  import { getTestConfig, TestConfig } from './config';
3
4
 
4
5
  const randomString = () => Math.random().toString(36).substring(7);