google-drive-mock 1.0.5 → 1.0.6
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/dist/index.js +123 -3
- package/dist/mappers.d.ts +9 -0
- package/dist/mappers.js +61 -0
- package/package.json +3 -2
- package/specs/google-drive-api-v2.json +6795 -0
- package/src/index.ts +152 -3
- package/src/mappers.ts +59 -0
- package/test/basics.test.ts +1 -0
- package/test/config.ts +26 -22
- package/test/etag.test.ts +53 -117
- package/test/v2_basics.test.ts +140 -0
- package/test/v3_parity.test.ts +96 -0
- package/vitest.config.ts +16 -0
package/dist/index.js
CHANGED
|
@@ -17,9 +17,12 @@ const express_1 = __importDefault(require("express"));
|
|
|
17
17
|
const cors_1 = __importDefault(require("cors"));
|
|
18
18
|
const store_1 = require("./store");
|
|
19
19
|
const batch_1 = require("./batch");
|
|
20
|
+
const mappers_1 = require("./mappers");
|
|
20
21
|
const createApp = (config = {}) => {
|
|
21
22
|
const app = (0, express_1.default)();
|
|
22
|
-
app.use((0, cors_1.default)(
|
|
23
|
+
app.use((0, cors_1.default)({
|
|
24
|
+
exposedHeaders: ['ETag']
|
|
25
|
+
}));
|
|
23
26
|
app.set('etag', false); // Disable default ETag generation to match Real API behavior
|
|
24
27
|
app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
28
|
if (config.serverLagBefore && config.serverLagBefore > 0) {
|
|
@@ -53,7 +56,8 @@ const createApp = (config = {}) => {
|
|
|
53
56
|
// Auth Middleware
|
|
54
57
|
const validTokens = ['valid-token', 'another-valid-token'];
|
|
55
58
|
app.use((req, res, next) => {
|
|
56
|
-
const
|
|
59
|
+
const authHeaderVal = req.headers.authorization;
|
|
60
|
+
const authHeader = Array.isArray(authHeaderVal) ? authHeaderVal[0] : authHeaderVal;
|
|
57
61
|
if (!authHeader) {
|
|
58
62
|
res.status(401).json({ error: { code: 401, message: "Unauthorized: No token provided" } });
|
|
59
63
|
return;
|
|
@@ -206,7 +210,8 @@ const createApp = (config = {}) => {
|
|
|
206
210
|
res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart is supported in this mock route" } });
|
|
207
211
|
return;
|
|
208
212
|
}
|
|
209
|
-
const
|
|
213
|
+
const contentTypeHeader = req.headers['content-type'];
|
|
214
|
+
const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
|
|
210
215
|
if (!contentType || !contentType.includes('multipart/related')) {
|
|
211
216
|
res.status(400).json({ error: { code: 400, message: "Content-Type must be multipart/related" } });
|
|
212
217
|
return;
|
|
@@ -335,6 +340,12 @@ const createApp = (config = {}) => {
|
|
|
335
340
|
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
336
341
|
return;
|
|
337
342
|
}
|
|
343
|
+
// Parity: Real V3 API returns 400 if 'etag' is requested in fields
|
|
344
|
+
const fields = req.query.fields;
|
|
345
|
+
if (fields && (fields.includes('etag') || fields.includes('kind,etag'))) {
|
|
346
|
+
res.status(400).json({ error: { code: 400, message: "Invalid field selection: etag" } });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
338
349
|
// Mock does not return ETag header because Real API (v3) does not return it by default/in this context.
|
|
339
350
|
// res.setHeader('ETag', etag);
|
|
340
351
|
// Real API also ignores If-None-Match if ETag is not supported?
|
|
@@ -410,6 +421,115 @@ const createApp = (config = {}) => {
|
|
|
410
421
|
}
|
|
411
422
|
res.status(204).send();
|
|
412
423
|
});
|
|
424
|
+
// ==========================================
|
|
425
|
+
// Google Drive API V2 Routes
|
|
426
|
+
// ==========================================
|
|
427
|
+
// V2 Files: Create
|
|
428
|
+
app.post('/drive/v2/files', (req, res) => {
|
|
429
|
+
const v2Body = req.body || {};
|
|
430
|
+
const fileData = (0, mappers_1.fromV2Update)(v2Body);
|
|
431
|
+
// V2 typical defaults
|
|
432
|
+
const name = fileData.name || v2Body.title || "Untitled"; // Fallback if mapper missed it or explicit
|
|
433
|
+
const newFile = store_1.driveStore.createFile(Object.assign(Object.assign({}, fileData), { name: name, mimeType: fileData.mimeType || "application/octet-stream", parents: fileData.parents || [] }));
|
|
434
|
+
res.status(200).json((0, mappers_1.toV2File)(newFile));
|
|
435
|
+
});
|
|
436
|
+
// V2 Files: Get
|
|
437
|
+
app.get('/drive/v2/files/:fileId', (req, res) => {
|
|
438
|
+
const fileId = req.params.fileId;
|
|
439
|
+
if (typeof fileId !== 'string') {
|
|
440
|
+
res.status(400).send("Invalid file ID");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const file = store_1.driveStore.getFile(fileId);
|
|
444
|
+
if (!file) {
|
|
445
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// V2 ETag handling - usually sends ETag header
|
|
449
|
+
if (file.etag) {
|
|
450
|
+
res.setHeader('ETag', file.etag);
|
|
451
|
+
}
|
|
452
|
+
res.json((0, mappers_1.toV2File)(file));
|
|
453
|
+
});
|
|
454
|
+
// V2 Files: Update (PUT)
|
|
455
|
+
app.put('/drive/v2/files/:fileId', (req, res) => {
|
|
456
|
+
const fileId = req.params.fileId;
|
|
457
|
+
if (typeof fileId !== 'string') {
|
|
458
|
+
res.status(400).send("Invalid file ID");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const v2Body = req.body || {};
|
|
462
|
+
const updates = (0, mappers_1.fromV2Update)(v2Body);
|
|
463
|
+
const existingFile = store_1.driveStore.getFile(fileId);
|
|
464
|
+
if (!existingFile) {
|
|
465
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Check for Precondition (If-Match)
|
|
469
|
+
const ifMatchHeader = req.headers['if-match'];
|
|
470
|
+
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
471
|
+
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
472
|
+
// Also support quoted etag if user sends it
|
|
473
|
+
// Internal etag might be "version", validation needs exact match
|
|
474
|
+
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
475
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const updatedFile = store_1.driveStore.updateFile(fileId, updates);
|
|
480
|
+
res.json((0, mappers_1.toV2File)(updatedFile));
|
|
481
|
+
});
|
|
482
|
+
// V2 Files: Patch (PATCH)
|
|
483
|
+
app.patch('/drive/v2/files/:fileId', (req, res) => {
|
|
484
|
+
const fileId = req.params.fileId;
|
|
485
|
+
if (typeof fileId !== 'string') {
|
|
486
|
+
res.status(400).send("Invalid file ID");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const v2Body = req.body || {};
|
|
490
|
+
const updates = (0, mappers_1.fromV2Update)(v2Body);
|
|
491
|
+
const existingFile = store_1.driveStore.getFile(fileId);
|
|
492
|
+
if (!existingFile) {
|
|
493
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Check for Precondition (If-Match)
|
|
497
|
+
const ifMatchHeader = req.headers['if-match'];
|
|
498
|
+
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
499
|
+
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
500
|
+
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
501
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const updatedFile = store_1.driveStore.updateFile(fileId, updates);
|
|
506
|
+
res.json((0, mappers_1.toV2File)(updatedFile));
|
|
507
|
+
});
|
|
508
|
+
// V2 Files: Delete
|
|
509
|
+
app.delete('/drive/v2/files/:fileId', (req, res) => {
|
|
510
|
+
const fileId = req.params.fileId;
|
|
511
|
+
if (typeof fileId !== 'string') {
|
|
512
|
+
res.status(400).send("Invalid file ID");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const existingFile = store_1.driveStore.getFile(fileId);
|
|
516
|
+
// V2 specific: often returns 404 for not found, same as V3 check
|
|
517
|
+
if (!existingFile) {
|
|
518
|
+
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Check for Precondition (If-Match) - V2 respects this more often
|
|
522
|
+
const ifMatchHeader = req.headers['if-match'];
|
|
523
|
+
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
524
|
+
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
525
|
+
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
526
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
store_1.driveStore.deleteFile(fileId);
|
|
531
|
+
res.status(204).send();
|
|
532
|
+
});
|
|
413
533
|
return app;
|
|
414
534
|
};
|
|
415
535
|
exports.createApp = createApp;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DriveFile } from './store';
|
|
2
|
+
/**
|
|
3
|
+
* Maps an internal DriveFile (V3 format) to a V2 API File resource.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toV2File(file: DriveFile): Record<string, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Maps a V2 API File Update/Insert body to a partial Internal DriveFile (V3 format).
|
|
8
|
+
*/
|
|
9
|
+
export declare function fromV2Update(body: Record<string, unknown>): Partial<DriveFile>;
|
package/dist/mappers.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toV2File = toV2File;
|
|
4
|
+
exports.fromV2Update = fromV2Update;
|
|
5
|
+
/**
|
|
6
|
+
* Maps an internal DriveFile (V3 format) to a V2 API File resource.
|
|
7
|
+
*/
|
|
8
|
+
function toV2File(file) {
|
|
9
|
+
return {
|
|
10
|
+
kind: 'drive#file',
|
|
11
|
+
id: file.id,
|
|
12
|
+
etag: file.etag || `"${file.version}"`, // V2 uses etags frequently
|
|
13
|
+
selfLink: `http://localhost/drive/v2/files/${file.id}`, // Mock link
|
|
14
|
+
title: file.name,
|
|
15
|
+
mimeType: file.mimeType,
|
|
16
|
+
labels: {
|
|
17
|
+
starred: file.starred || false,
|
|
18
|
+
hidden: false,
|
|
19
|
+
trashed: file.trashed || false,
|
|
20
|
+
restricted: false,
|
|
21
|
+
viewed: true
|
|
22
|
+
},
|
|
23
|
+
createdDate: file.createdTime,
|
|
24
|
+
modifiedDate: file.modifiedTime,
|
|
25
|
+
parents: (file.parents || []).map(parentId => ({
|
|
26
|
+
kind: 'drive#parentReference',
|
|
27
|
+
id: parentId,
|
|
28
|
+
selfLink: `http://localhost/drive/v2/files/${parentId}`,
|
|
29
|
+
parentLink: `http://localhost/drive/v2/files/${parentId}`,
|
|
30
|
+
isRoot: false // Mock simplification
|
|
31
|
+
})),
|
|
32
|
+
version: file.version,
|
|
33
|
+
downloadUrl: `http://localhost/drive/v2/files/${file.id}?alt=media`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Maps a V2 API File Update/Insert body to a partial Internal DriveFile (V3 format).
|
|
38
|
+
*/
|
|
39
|
+
function fromV2Update(body) {
|
|
40
|
+
const update = {};
|
|
41
|
+
if (typeof body.title === 'string')
|
|
42
|
+
update.name = body.title;
|
|
43
|
+
if (typeof body.mimeType === 'string')
|
|
44
|
+
update.mimeType = body.mimeType;
|
|
45
|
+
if (typeof body.modifiedDate === 'string')
|
|
46
|
+
update.modifiedTime = body.modifiedDate;
|
|
47
|
+
// Parents in V2 create are typically [{id: '...'}]
|
|
48
|
+
if (body.parents && Array.isArray(body.parents)) {
|
|
49
|
+
update.parents = body.parents
|
|
50
|
+
.map((p) => p.id)
|
|
51
|
+
.filter((id) => typeof id === 'string');
|
|
52
|
+
}
|
|
53
|
+
if (body.labels && typeof body.labels === 'object') {
|
|
54
|
+
const labels = body.labels;
|
|
55
|
+
if (typeof labels.starred === 'boolean')
|
|
56
|
+
update.starred = labels.starred;
|
|
57
|
+
if (typeof labels.trashed === 'boolean')
|
|
58
|
+
update.trashed = labels.trashed;
|
|
59
|
+
}
|
|
60
|
+
return update;
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-drive-mock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"dev": "ts-node src/index.ts",
|
|
11
11
|
"test": "TEST_TARGET=mock vitest run",
|
|
12
12
|
"test:slow": "LATENCY=20 vitest run",
|
|
13
|
-
"test:browser": "start-server-and-test dev http://localhost:3000 'BROWSER_ENABLED=true vitest run --browser'",
|
|
13
|
+
"test:browser": "start-server-and-test dev http://localhost:3000 'TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser'",
|
|
14
|
+
"test:browser:real": "TEST_TARGET=real BROWSER_ENABLED=true vitest run --browser",
|
|
14
15
|
"test:real": "TEST_TARGET=real vitest run",
|
|
15
16
|
"example:login": "ts-node examples/serve-login.ts",
|
|
16
17
|
"lint": "eslint .",
|