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
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
|
-
-
|
|
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
|
-
|
|
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 !== '*'
|
|
120
|
-
|
|
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 !== '*'
|
|
164
|
-
|
|
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 !== '*'
|
|
408
|
-
|
|
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 !== '*'
|
|
691
|
-
|
|
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;
|
|
@@ -249,11 +249,17 @@ const createV3Router = () => {
|
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
251
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
|
+
});
|
|
252
258
|
res.json({
|
|
253
259
|
kind: "drive#changeList",
|
|
254
260
|
newStartPageToken: result.newStartPageToken,
|
|
255
261
|
nextPageToken: result.nextPageToken,
|
|
256
|
-
changes:
|
|
262
|
+
changes: mappedChanges
|
|
257
263
|
});
|
|
258
264
|
});
|
|
259
265
|
// Upload Files Route
|
|
@@ -277,7 +283,7 @@ const createV3Router = () => {
|
|
|
277
283
|
parents: [],
|
|
278
284
|
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content)
|
|
279
285
|
});
|
|
280
|
-
res.status(200).json(newFile);
|
|
286
|
+
res.status(200).json((0, mappers_1.toV3File)(newFile));
|
|
281
287
|
return;
|
|
282
288
|
}
|
|
283
289
|
const contentTypeHeader = req.headers['content-type'];
|
|
@@ -339,7 +345,7 @@ const createV3Router = () => {
|
|
|
339
345
|
content = contentPart.body;
|
|
340
346
|
}
|
|
341
347
|
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, metadata), { content: content }));
|
|
342
|
-
res.status(200).json(newFile);
|
|
348
|
+
res.status(200).json((0, mappers_1.toV3File)(newFile));
|
|
343
349
|
});
|
|
344
350
|
// Upload Files: Update (PATCH)
|
|
345
351
|
app.patch('/upload/drive/v3/files/:fileId', (req, res) => {
|
|
@@ -353,6 +359,16 @@ const createV3Router = () => {
|
|
|
353
359
|
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
354
360
|
return;
|
|
355
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
|
+
}
|
|
356
372
|
const uploadType = req.query.uploadType;
|
|
357
373
|
if (uploadType === 'media') {
|
|
358
374
|
const rawBody = req.body;
|
|
@@ -363,7 +379,7 @@ const createV3Router = () => {
|
|
|
363
379
|
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content),
|
|
364
380
|
modifiedTime: new Date().toISOString()
|
|
365
381
|
});
|
|
366
|
-
res.status(200).json(updatedFile);
|
|
382
|
+
res.status(200).json((0, mappers_1.toV3File)(updatedFile));
|
|
367
383
|
return;
|
|
368
384
|
}
|
|
369
385
|
const contentTypeHeader = req.headers['content-type'];
|
|
@@ -424,7 +440,7 @@ const createV3Router = () => {
|
|
|
424
440
|
}
|
|
425
441
|
// Perform update
|
|
426
442
|
const updatedFile = store_1.driveStore.updateFile(fileId, Object.assign(Object.assign({}, metadata), { content: content, modifiedTime: new Date().toISOString() }));
|
|
427
|
-
res.status(200).json(updatedFile);
|
|
443
|
+
res.status(200).json((0, mappers_1.toV3File)(updatedFile));
|
|
428
444
|
return;
|
|
429
445
|
}
|
|
430
446
|
res.status(400).json({ error: { code: 400, message: "Only uploadType=media or multipart/related is supported for V3 PATCH upload" } });
|
|
@@ -434,7 +450,7 @@ const createV3Router = () => {
|
|
|
434
450
|
const body = req.body || {};
|
|
435
451
|
const name = body.name || "Untitled";
|
|
436
452
|
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, body), { name: name, mimeType: body.mimeType || "application/octet-stream", parents: body.parents || [] }));
|
|
437
|
-
res.status(200).json(newFile);
|
|
453
|
+
res.status(200).json((0, mappers_1.toV3File)(newFile));
|
|
438
454
|
});
|
|
439
455
|
// Files: Get
|
|
440
456
|
app.get('/drive/v3/files/:fileId', (req, res) => {
|
|
@@ -484,7 +500,12 @@ const createV3Router = () => {
|
|
|
484
500
|
}
|
|
485
501
|
return;
|
|
486
502
|
}
|
|
487
|
-
|
|
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);
|
|
488
509
|
});
|
|
489
510
|
// Files: Update
|
|
490
511
|
app.patch('/drive/v3/files/:fileId', (req, res) => {
|
|
@@ -526,7 +547,7 @@ const createV3Router = () => {
|
|
|
526
547
|
Object.assign(updatedFile, result);
|
|
527
548
|
}
|
|
528
549
|
}
|
|
529
|
-
res.json(updatedFile);
|
|
550
|
+
res.json((0, mappers_1.toV3File)(updatedFile));
|
|
530
551
|
});
|
|
531
552
|
// Files: Delete
|
|
532
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:
|
|
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.
|
|
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:
|
|
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"
|
package/scripts/check-token.ts
CHANGED
|
@@ -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, '');
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
let data = '';
|
|
69
|
+
console.log(`🧹 Found ${folders.length} google-drive-mock folders to clean up.`);
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
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
|
-
|
|
71
|
-
console.
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 !== '*'
|
|
136
|
-
|
|
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 !== '*'
|
|
187
|
-
|
|
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 !== '*'
|
|
473
|
-
|
|
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 !== '*'
|
|
810
|
-
|
|
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
|
}
|