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.
- 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 +59 -10
- 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 +65 -11
- 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 +175 -74
- 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 +60 -18
- 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;
|
|
@@ -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:
|
|
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({
|
|
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
|
-
|
|
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:
|
|
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
|
}
|