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.
- package/AGENTS.md +4 -1
- package/CLAUDE.md +17 -0
- package/dist/index.js +2 -1
- package/dist/mappers.d.ts +5 -0
- package/dist/mappers.js +15 -0
- package/dist/routes/v2.js +16 -8
- package/dist/routes/v3.js +30 -9
- package/dist/store.js +2 -2
- package/package.json +4 -4
- package/scripts/check-token.ts +107 -38
- package/scripts/run-loop.sh +18 -0
- package/src/index.ts +2 -1
- package/src/mappers.ts +15 -0
- package/src/routes/v2.ts +16 -8
- package/src/routes/v3.ts +35 -10
- package/src/store.ts +2 -2
- package/test/advanced_changes.test.ts +76 -29
- package/test/advanced_ordering.test.ts +2 -1
- package/test/basics.test.ts +34 -58
- package/test/batch_and_query.test.ts +28 -62
- package/test/batch_insert_download.test.ts +2 -1
- package/test/check_empty.test.ts +60 -0
- package/test/complex_query.test.ts +2 -1
- package/test/concurrent_fetch.test.ts +2 -1
- package/test/config.ts +164 -7
- package/test/dates_and_sorting.test.ts +2 -1
- package/test/etag.test.ts +8 -4
- package/test/features.test.ts +2 -1
- package/test/folder_query.test.ts +2 -1
- package/test/folder_search.test.ts +2 -1
- package/test/iterate_changes.test.ts +153 -68
- package/test/latency.test.ts +2 -1
- package/test/mime_types.test.ts +2 -1
- package/test/missing_fields.test.ts +2 -1
- package/test/multipart_behavior.test.ts +2 -1
- package/test/parallel_update.test.ts +2 -1
- package/test/parity_media_download.test.ts +2 -1
- package/test/routines.test.ts +15 -12
- package/test/upload.test.ts +2 -1
- package/test/url_parameters.test.ts +2 -1
- package/test/v2_basics.test.ts +22 -13
- package/test/v2_content.test.ts +2 -1
- package/test/v2_missing_ops.test.ts +75 -75
- package/test/v2_routes.test.ts +31 -21
- package/test/v2_upload.test.ts +17 -9
- package/test/v3_parity.test.ts +56 -20
- package/test_etag_headers.ts +92 -0
- 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,
|
|
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', () => {
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
137
|
+
title,
|
|
134
138
|
parents: [{ id: config.testFolderId }]
|
|
135
139
|
});
|
|
136
140
|
const fileId = createRes.body.id;
|
package/test/features.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe,
|
|
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,
|
|
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,
|
|
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);
|