google-drive-mock 1.1.5 → 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 +59 -10
  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 +65 -11
  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 +175 -74
  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 +60 -18
  47. package/test_etag_headers.ts +92 -0
  48. package/vitest.config.ts +7 -1
package/AGENTS.md CHANGED
@@ -12,4 +12,7 @@ After making changes to the codebase or to the tests, always run the following c
12
12
 
13
13
  - Whenever the `npm test:real` fails, fix the tests to match the "real" backend.
14
14
  - Ensure that the behavior of the mock is exactly equal to the "real" backend.
15
- - The header field "If-None-Match" does not work in google drive. Do never use it or assume it works.
15
+ - Always use random or unique file/folder names (e.g., using `Math.random().toString(36)`) when creating files or folders in tests to prevent naming collisions and leftover state on the real Google Drive API.
16
+ - Tests must be designed to run in parallel. Do not use global state resets or database cleanups (such as `config.clear()`) in `beforeEach`/`afterEach` hooks, as this pollutes/resets the state for other concurrently running test files.
17
+ - The header field "If-None-Match" does not work in google drive. Do never use it or assume it works.
18
+ - Do never add any hacky mock-only endpoints or custom APIs/debug parameters (e.g. for locking or syncing) to the mock server, as the mock server must behave exactly like the real API.
package/CLAUDE.md ADDED
@@ -0,0 +1,17 @@
1
+ # Claude Guide
2
+
3
+ ## Commands
4
+ - Build project: `npm run build`
5
+ - Run mock tests: `npm test`
6
+ - Run browser tests: `npm run test:browser`
7
+ - Run real API tests: `npm run test:real`
8
+ - Lint code: `npm run lint`
9
+ - Fix lint: `npm run lint:fix`
10
+
11
+ ## Rules
12
+ - Whenever the `npm test:real` fails, fix the tests to match the "real" backend.
13
+ - Ensure that the behavior of the mock is exactly equal to the "real" backend.
14
+ - Always use random or unique file/folder names (e.g., using `Math.random().toString(36)`) when creating files or folders in tests to prevent naming collisions and leftover state on the real Google Drive API.
15
+ - Tests must be designed to run in parallel. Do not use global state resets or database cleanups (such as `config.clear()`) in `beforeEach`/`afterEach` hooks, as this pollutes/resets the state for other concurrently running test files.
16
+ - The header field "If-None-Match" does not work in google drive. Do never use it or assume it works.
17
+ - Do never add any hacky mock-only endpoints or custom APIs/debug parameters (e.g. for locking or syncing) to the mock server, as the mock server must behave exactly like the real API.
package/dist/index.js CHANGED
@@ -105,5 +105,6 @@ const startServer = (port, host = 'localhost', config = {}) => {
105
105
  };
106
106
  exports.startServer = startServer;
107
107
  if (require.main === module) {
108
- startServer(3000);
108
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
109
+ startServer(port);
109
110
  }
package/dist/mappers.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import { DriveFile } from './store';
2
+ /**
3
+ * Maps an internal DriveFile (V3 format) to a V3 API File resource.
4
+ * Crucially, in V3, the etag field is NOT returned in the body of the File resource.
5
+ */
6
+ export declare function toV3File(file: DriveFile): Record<string, unknown>;
2
7
  /**
3
8
  * Maps an internal DriveFile (V3 format) to a V2 API File resource.
4
9
  */
package/dist/mappers.js CHANGED
@@ -1,8 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toV3File = toV3File;
3
4
  exports.toV2File = toV2File;
4
5
  exports.fromV2Update = fromV2Update;
5
6
  exports.applyFields = applyFields;
7
+ /**
8
+ * Maps an internal DriveFile (V3 format) to a V3 API File resource.
9
+ * Crucially, in V3, the etag field is NOT returned in the body of the File resource.
10
+ */
11
+ function toV3File(file) {
12
+ const v3 = {};
13
+ for (const key of Object.keys(file)) {
14
+ if (key === 'etag' || key === 'content') {
15
+ continue;
16
+ }
17
+ v3[key] = file[key];
18
+ }
19
+ return v3;
20
+ }
6
21
  /**
7
22
  * Maps an internal DriveFile (V3 format) to a V2 API File resource.
8
23
  */
package/dist/routes/v2.js CHANGED
@@ -116,8 +116,10 @@ const createV2Router = (config) => {
116
116
  }
117
117
  const ifMatchHeader = req.headers['if-match'];
118
118
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
119
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
120
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
119
+ if (ifMatch && ifMatch !== '*') {
120
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
121
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
122
+ if (cleanIfMatch !== cleanEtag) {
121
123
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
122
124
  return;
123
125
  }
@@ -160,8 +162,10 @@ const createV2Router = (config) => {
160
162
  }
161
163
  const ifMatchHeader = req.headers['if-match'];
162
164
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
163
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
164
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
165
+ if (ifMatch && ifMatch !== '*') {
166
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
167
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
168
+ if (cleanIfMatch !== cleanEtag) {
165
169
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
166
170
  return;
167
171
  }
@@ -404,8 +408,10 @@ const createV2Router = (config) => {
404
408
  }
405
409
  const ifMatchHeader = req.headers['if-match'];
406
410
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
407
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
408
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
411
+ if (ifMatch && ifMatch !== '*') {
412
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
413
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
414
+ if (cleanIfMatch !== cleanEtag) {
409
415
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
410
416
  return;
411
417
  }
@@ -687,8 +693,10 @@ const createV2Router = (config) => {
687
693
  // Check for Precondition (If-Match) - V2 respects this more often
688
694
  const ifMatchHeader = req.headers['if-match'];
689
695
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
690
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
691
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
696
+ if (ifMatch && ifMatch !== '*') {
697
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
698
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
699
+ if (cleanIfMatch !== cleanEtag) {
692
700
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
693
701
  return;
694
702
  }
package/dist/routes/v3.js CHANGED
@@ -197,7 +197,7 @@ const createV3Router = () => {
197
197
  }
198
198
  }
199
199
  const totalFiles = files.length;
200
- const resultFiles = files.slice(skip, skip + pageSize);
200
+ const resultFiles = files.slice(skip, skip + pageSize).map(mappers_1.toV3File);
201
201
  let nextPageToken;
202
202
  if (skip + pageSize < totalFiles) {
203
203
  const nextSkip = skip + pageSize;
@@ -210,6 +210,22 @@ const createV3Router = () => {
210
210
  nextPageToken
211
211
  };
212
212
  const fields = req.query.fields;
213
+ if (fields && (fields.includes('etag') || fields.includes('kind,etag'))) {
214
+ res.status(400).json({
215
+ error: {
216
+ code: 400,
217
+ message: "Invalid field selection etag",
218
+ errors: [{
219
+ message: "Invalid field selection etag",
220
+ domain: "global",
221
+ reason: "invalidParameter",
222
+ location: "fields",
223
+ locationType: "parameter"
224
+ }]
225
+ }
226
+ });
227
+ return;
228
+ }
213
229
  if (fields) {
214
230
  res.json((0, mappers_1.applyFields)(response, fields));
215
231
  }
@@ -233,11 +249,17 @@ const createV3Router = () => {
233
249
  return;
234
250
  }
235
251
  const result = store_1.driveStore.getChanges(pageToken);
252
+ const mappedChanges = result.changes.map(c => {
253
+ if (c.file) {
254
+ return Object.assign(Object.assign({}, c), { file: (0, mappers_1.toV3File)(c.file) });
255
+ }
256
+ return c;
257
+ });
236
258
  res.json({
237
259
  kind: "drive#changeList",
238
260
  newStartPageToken: result.newStartPageToken,
239
261
  nextPageToken: result.nextPageToken,
240
- changes: result.changes
262
+ changes: mappedChanges
241
263
  });
242
264
  });
243
265
  // Upload Files Route
@@ -261,7 +283,7 @@ const createV3Router = () => {
261
283
  parents: [],
262
284
  content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content)
263
285
  });
264
- res.status(200).json(newFile);
286
+ res.status(200).json((0, mappers_1.toV3File)(newFile));
265
287
  return;
266
288
  }
267
289
  const contentTypeHeader = req.headers['content-type'];
@@ -323,7 +345,7 @@ const createV3Router = () => {
323
345
  content = contentPart.body;
324
346
  }
325
347
  const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, metadata), { content: content }));
326
- res.status(200).json(newFile);
348
+ res.status(200).json((0, mappers_1.toV3File)(newFile));
327
349
  });
328
350
  // Upload Files: Update (PATCH)
329
351
  app.patch('/upload/drive/v3/files/:fileId', (req, res) => {
@@ -337,6 +359,16 @@ const createV3Router = () => {
337
359
  res.status(404).json({ error: { code: 404, message: "File not found" } });
338
360
  return;
339
361
  }
362
+ const ifMatchHeader = req.headers['if-match'];
363
+ const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
364
+ if (ifMatch && ifMatch !== '*') {
365
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
366
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
367
+ if (cleanIfMatch !== cleanEtag) {
368
+ res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
369
+ return;
370
+ }
371
+ }
340
372
  const uploadType = req.query.uploadType;
341
373
  if (uploadType === 'media') {
342
374
  const rawBody = req.body;
@@ -347,7 +379,7 @@ const createV3Router = () => {
347
379
  content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content),
348
380
  modifiedTime: new Date().toISOString()
349
381
  });
350
- res.status(200).json(updatedFile);
382
+ res.status(200).json((0, mappers_1.toV3File)(updatedFile));
351
383
  return;
352
384
  }
353
385
  const contentTypeHeader = req.headers['content-type'];
@@ -408,7 +440,7 @@ const createV3Router = () => {
408
440
  }
409
441
  // Perform update
410
442
  const updatedFile = store_1.driveStore.updateFile(fileId, Object.assign(Object.assign({}, metadata), { content: content, modifiedTime: new Date().toISOString() }));
411
- res.status(200).json(updatedFile);
443
+ res.status(200).json((0, mappers_1.toV3File)(updatedFile));
412
444
  return;
413
445
  }
414
446
  res.status(400).json({ error: { code: 400, message: "Only uploadType=media or multipart/related is supported for V3 PATCH upload" } });
@@ -418,7 +450,7 @@ const createV3Router = () => {
418
450
  const body = req.body || {};
419
451
  const name = body.name || "Untitled";
420
452
  const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, body), { name: name, mimeType: body.mimeType || "application/octet-stream", parents: body.parents || [] }));
421
- res.status(200).json(newFile);
453
+ res.status(200).json((0, mappers_1.toV3File)(newFile));
422
454
  });
423
455
  // Files: Get
424
456
  app.get('/drive/v3/files/:fileId', (req, res) => {
@@ -434,7 +466,19 @@ const createV3Router = () => {
434
466
  }
435
467
  const fields = req.query.fields;
436
468
  if (fields && (fields.includes('etag') || fields.includes('kind,etag'))) {
437
- res.status(400).json({ error: { code: 400, message: "Invalid field selection: etag" } });
469
+ res.status(400).json({
470
+ error: {
471
+ code: 400,
472
+ message: "Invalid field selection etag",
473
+ errors: [{
474
+ message: "Invalid field selection etag",
475
+ domain: "global",
476
+ reason: "invalidParameter",
477
+ location: "fields",
478
+ locationType: "parameter"
479
+ }]
480
+ }
481
+ });
438
482
  return;
439
483
  }
440
484
  if (req.query.alt === 'media') {
@@ -456,7 +500,12 @@ const createV3Router = () => {
456
500
  }
457
501
  return;
458
502
  }
459
- res.json(file);
503
+ const v3File = (0, mappers_1.toV3File)(file);
504
+ if (fields) {
505
+ res.json((0, mappers_1.applyFields)(v3File, fields));
506
+ return;
507
+ }
508
+ res.json(v3File);
460
509
  });
461
510
  // Files: Update
462
511
  app.patch('/drive/v3/files/:fileId', (req, res) => {
@@ -498,7 +547,7 @@ const createV3Router = () => {
498
547
  Object.assign(updatedFile, result);
499
548
  }
500
549
  }
501
- res.json(updatedFile);
550
+ res.json((0, mappers_1.toV3File)(updatedFile));
502
551
  });
503
552
  // Files: Delete
504
553
  app.delete('/drive/v3/files/:fileId', (req, res) => {
package/dist/store.js CHANGED
@@ -74,7 +74,7 @@ class DriveStore {
74
74
  const id = file.id || generateDriveFileId();
75
75
  const now = new Date().toISOString();
76
76
  const stats = this.calculateStats(file.content);
77
- const newFile = Object.assign(Object.assign({ kind: "drive#file", mimeType: "application/octet-stream", trashed: false, createdTime: now, modifiedTime: now }, file), { id, version: 1, etag: "1",
77
+ const newFile = Object.assign(Object.assign({ kind: "drive#file", mimeType: "application/octet-stream", trashed: false, createdTime: now, modifiedTime: now }, file), { id, version: 1, etag: '"1"',
78
78
  // Ensure calculated stats override provided ones
79
79
  size: stats.size, md5Checksum: stats.md5Checksum });
80
80
  this.files.set(id, newFile);
@@ -92,7 +92,7 @@ class DriveStore {
92
92
  }
93
93
  // Merge updates and increment version
94
94
  const newVersion = file.version + 1;
95
- const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { version: newVersion, etag: String(newVersion), modifiedTime: updates.modifiedTime || new Date().toISOString() });
95
+ const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { version: newVersion, etag: `"${newVersion}"`, modifiedTime: updates.modifiedTime || new Date().toISOString() });
96
96
  this.files.set(id, updatedFile);
97
97
  this.addChange(updatedFile);
98
98
  return updatedFile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-drive-mock",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Mock-Server that simulates being google-drive. Used for testing.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,11 +8,11 @@
8
8
  "build": "tsc",
9
9
  "start": "node dist/index.js",
10
10
  "dev": "ts-node src/index.ts",
11
- "test": "TEST_TARGET=mock vitest run",
11
+ "test": "PORT=3080 start-server-and-test dev http://localhost:3080 'PORT=3080 TEST_TARGET=mock USE_SHARED_MOCK=true vitest run --fileParallelism=false --exclude \"**/v2_routes.test.ts\" --exclude \"**/check_empty.test.ts\" && PORT=3080 TEST_TARGET=mock USE_SHARED_MOCK=true vitest run --fileParallelism=false test/v2_routes.test.ts && PORT=3080 TEST_TARGET=mock USE_SHARED_MOCK=true vitest run --fileParallelism=false test/check_empty.test.ts'",
12
12
  "test:slow": "LATENCY=20 vitest run",
13
- "test:browser": "start-server-and-test dev http://localhost:3000 'TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser --no-file-parallelism'",
13
+ "test:browser": "PORT=3009 start-server-and-test dev http://localhost:3009 'TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser=chromium --fileParallelism=false --exclude \"**/v2_routes.test.ts\" --exclude \"**/check_empty.test.ts\" && TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser=chromium --fileParallelism=false test/v2_routes.test.ts && TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser=chromium --fileParallelism=false test/check_empty.test.ts'",
14
14
  "test:browser:real": "TEST_TARGET=real BROWSER_ENABLED=true vitest run --browser",
15
- "test:real": "ts-node scripts/check-token.ts && TEST_TARGET=real vitest run",
15
+ "test:real": "ts-node scripts/check-token.ts && TEST_TARGET=real vitest run --exclude '**/v2_routes.test.ts' --exclude '**/check_empty.test.ts' && TEST_TARGET=real vitest run test/v2_routes.test.ts && TEST_TARGET=real vitest run test/check_empty.test.ts",
16
16
  "example:login": "ts-node examples/serve-login.ts",
17
17
  "lint": "eslint .",
18
18
  "lint:fix": "eslint . --fix"
@@ -2,8 +2,6 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import * as https from 'https';
4
4
 
5
- // Load .ENV manually to avoid devDependency issues if dotenv isn't available in this context,
6
- // though project seems to use it.
7
5
  const envPath = path.resolve(__dirname, '../.ENV');
8
6
  if (fs.existsSync(envPath)) {
9
7
  const envContent = fs.readFileSync(envPath, 'utf8');
@@ -11,7 +9,7 @@ if (fs.existsSync(envPath)) {
11
9
  const match = line.match(/^([^=]+)=(.*)$/);
12
10
  if (match) {
13
11
  const key = match[1].trim();
14
- const value = match[2].trim().replace(/^['"]|['"]$/g, ''); // strip quotes
12
+ const value = match[2].trim().replace(/^['"]|['"]$/g, '');
15
13
  if (!process.env[key]) {
16
14
  process.env[key] = value;
17
15
  }
@@ -26,50 +24,121 @@ if (!token) {
26
24
  process.exit(1);
27
25
  }
28
26
 
29
- // Simple check only if running real tests (though script is likely invoked specifically for that)
30
- // The user asked to run this BEFORE test:real.
31
-
32
- console.log('🔄 Verifying GDRIVE_TOKEN...');
27
+ function makeRequest(options: https.RequestOptions, postData?: string): Promise<{ statusCode?: number; data: string }> {
28
+ return new Promise((resolve, reject) => {
29
+ const req = https.request(options, (res) => {
30
+ let data = '';
31
+ res.on('data', (chunk) => data += chunk);
32
+ res.on('end', () => resolve({ statusCode: res.statusCode, data }));
33
+ });
34
+ req.on('error', reject);
35
+ if (postData) req.write(postData);
36
+ req.end();
37
+ });
38
+ }
33
39
 
34
- const options = {
35
- hostname: 'www.googleapis.com',
36
- path: '/drive/v3/about?fields=user',
37
- method: 'GET',
38
- headers: {
40
+ async function cleanTestFolder(token: string) {
41
+ const headers = {
39
42
  'Authorization': `Bearer ${token}`,
40
43
  'User-Agent': 'node-script'
44
+ };
45
+
46
+ // 1. Search for all google-drive-mock folders
47
+ const folderName = 'google-drive-mock';
48
+ const query = `mimeType='application/vnd.google-apps.folder' and name='${folderName}' and trashed=false`;
49
+ const searchOptions: https.RequestOptions = {
50
+ hostname: 'www.googleapis.com',
51
+ path: `/drive/v3/files?q=${encodeURIComponent(query)}&pageSize=100`,
52
+ method: 'GET',
53
+ headers
54
+ };
55
+
56
+ const searchRes = await makeRequest(searchOptions);
57
+ if (searchRes.statusCode !== 200) {
58
+ console.error('❌ Failed to search for test folders:', searchRes.data);
59
+ return;
60
+ }
61
+
62
+ const searchData = JSON.parse(searchRes.data);
63
+ const folders = searchData.files || [];
64
+ if (folders.length === 0) {
65
+ console.log('ℹ️ No google-drive-mock test folders exist yet.');
66
+ return;
41
67
  }
42
- };
43
68
 
44
- const req = https.request(options, (res) => {
45
- let data = '';
69
+ console.log(`🧹 Found ${folders.length} google-drive-mock folders to clean up.`);
46
70
 
47
- res.on('data', (chunk) => {
48
- data += chunk;
49
- });
71
+ for (const folder of folders) {
72
+ const folderId = folder.id;
73
+ // List all files inside this folder
74
+ const listQuery = `'${folderId}' in parents and trashed=false`;
75
+ const listOptions: https.RequestOptions = {
76
+ hostname: 'www.googleapis.com',
77
+ path: `/drive/v3/files?q=${encodeURIComponent(listQuery)}&pageSize=100`,
78
+ method: 'GET',
79
+ headers
80
+ };
50
81
 
51
- res.on('end', () => {
52
- if (res.statusCode === 200) {
53
- try {
54
- const body = JSON.parse(data);
55
- console.log(`✅ Token is valid. User: ${body.user?.emailAddress || 'Unknown'}`);
56
- process.exit(0);
57
- } catch (e: unknown) {
58
- const msg = e instanceof Error ? e.message : String(e);
59
- console.error('❌ Error parsing response:', msg);
60
- process.exit(1);
82
+ const listRes = await makeRequest(listOptions);
83
+ if (listRes.statusCode === 200) {
84
+ const listData = JSON.parse(listRes.data);
85
+ const files = listData.files || [];
86
+ for (const file of files) {
87
+ const deleteOptions: https.RequestOptions = {
88
+ hostname: 'www.googleapis.com',
89
+ path: `/drive/v3/files/${file.id}`,
90
+ method: 'DELETE',
91
+ headers
92
+ };
93
+ await makeRequest(deleteOptions);
61
94
  }
95
+ }
96
+
97
+ // Delete the folder itself
98
+ const deleteFolderOptions: https.RequestOptions = {
99
+ hostname: 'www.googleapis.com',
100
+ path: `/drive/v3/files/${folderId}`,
101
+ method: 'DELETE',
102
+ headers
103
+ };
104
+ const delRes = await makeRequest(deleteFolderOptions);
105
+ if (delRes.statusCode === 204 || delRes.statusCode === 200) {
106
+ console.log(` Deleted folder: ${folder.name} (${folderId})`);
62
107
  } else {
63
- console.error(`❌ Token verification failed. Tell the human to update the .ENV file with a valid token. Status: ${res.statusCode}`);
64
- console.error('Response:', data);
65
- process.exit(1);
108
+ console.warn(` ⚠️ Failed to delete folder ${folder.name} (${folderId}): Status ${delRes.statusCode}`);
66
109
  }
67
- });
68
- });
110
+ }
111
+ }
69
112
 
70
- req.on('error', (e) => {
71
- console.error(`❌ Request error: ${e.message}`);
72
- process.exit(1);
73
- });
113
+ async function main() {
114
+ console.log('🔄 Verifying GDRIVE_TOKEN...');
115
+ const options: https.RequestOptions = {
116
+ hostname: 'www.googleapis.com',
117
+ path: '/drive/v3/about?fields=user',
118
+ method: 'GET',
119
+ headers: {
120
+ 'Authorization': `Bearer ${token}`,
121
+ 'User-Agent': 'node-script'
122
+ }
123
+ };
124
+
125
+ try {
126
+ const res = await makeRequest(options);
127
+ if (res.statusCode === 200) {
128
+ const body = JSON.parse(res.data);
129
+ console.log(`✅ Token is valid. User: ${body.user?.emailAddress || 'Unknown'}`);
130
+ await cleanTestFolder(token!);
131
+ process.exit(0);
132
+ } else {
133
+ console.error(`❌ Token verification failed. Status: ${res.statusCode}`);
134
+ console.error('Response:', res.data);
135
+ process.exit(1);
136
+ }
137
+ } catch (e: unknown) {
138
+ const msg = e instanceof Error ? e.message : String(e);
139
+ console.error('❌ Error verifying token:', msg);
140
+ process.exit(1);
141
+ }
142
+ }
74
143
 
75
- req.end();
144
+ main();
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Starting 30 test:real iterations..."
5
+ for i in $(seq 1 30); do
6
+ echo "========================================"
7
+ echo "Iteration $i/30 started"
8
+ echo "========================================"
9
+
10
+ # Run the test:real script
11
+ npm run test:real
12
+
13
+ echo "========================================"
14
+ echo "Iteration $i/30 PASSED"
15
+ echo "========================================"
16
+ done
17
+
18
+ echo "All 30 iterations completed successfully!"
package/src/index.ts CHANGED
@@ -105,7 +105,8 @@ const startServer = (port: number, host: string = 'localhost', config: AppConfig
105
105
  };
106
106
 
107
107
  if (require.main === module) {
108
- startServer(3000);
108
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
109
+ startServer(port);
109
110
  }
110
111
 
111
112
  export { createApp, startServer };
package/src/mappers.ts CHANGED
@@ -1,5 +1,20 @@
1
1
  import { DriveFile } from './store';
2
2
 
3
+ /**
4
+ * Maps an internal DriveFile (V3 format) to a V3 API File resource.
5
+ * Crucially, in V3, the etag field is NOT returned in the body of the File resource.
6
+ */
7
+ export function toV3File(file: DriveFile): Record<string, unknown> {
8
+ const v3: Record<string, unknown> = {};
9
+ for (const key of Object.keys(file)) {
10
+ if (key === 'etag' || key === 'content') {
11
+ continue;
12
+ }
13
+ v3[key] = file[key];
14
+ }
15
+ return v3;
16
+ }
17
+
3
18
  /**
4
19
  * Maps an internal DriveFile (V3 format) to a V2 API File resource.
5
20
  */
package/src/routes/v2.ts CHANGED
@@ -132,8 +132,10 @@ export const createV2Router = (config: AppConfig) => {
132
132
 
133
133
  const ifMatchHeader = req.headers['if-match'];
134
134
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
135
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
136
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
135
+ if (ifMatch && ifMatch !== '*') {
136
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
137
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
138
+ if (cleanIfMatch !== cleanEtag) {
137
139
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
138
140
  return;
139
141
  }
@@ -183,8 +185,10 @@ export const createV2Router = (config: AppConfig) => {
183
185
 
184
186
  const ifMatchHeader = req.headers['if-match'];
185
187
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
186
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
187
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
188
+ if (ifMatch && ifMatch !== '*') {
189
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
190
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
191
+ if (cleanIfMatch !== cleanEtag) {
188
192
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
189
193
  return;
190
194
  }
@@ -469,8 +473,10 @@ export const createV2Router = (config: AppConfig) => {
469
473
 
470
474
  const ifMatchHeader = req.headers['if-match'];
471
475
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
472
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
473
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
476
+ if (ifMatch && ifMatch !== '*') {
477
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
478
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
479
+ if (cleanIfMatch !== cleanEtag) {
474
480
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
475
481
  return;
476
482
  }
@@ -806,8 +812,10 @@ export const createV2Router = (config: AppConfig) => {
806
812
  // Check for Precondition (If-Match) - V2 respects this more often
807
813
  const ifMatchHeader = req.headers['if-match'];
808
814
  const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
809
- if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
810
- if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
815
+ if (ifMatch && ifMatch !== '*') {
816
+ const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
817
+ const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
818
+ if (cleanIfMatch !== cleanEtag) {
811
819
  res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
812
820
  return;
813
821
  }