google-drive-mock 1.1.2 → 1.1.4
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/README.md +2 -1
- package/dist/index.js +11 -2
- package/dist/routes/v3.js +12 -5
- package/dist/store.js +3 -0
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
- package/src/index.ts +12 -3
- package/src/routes/v3.ts +13 -5
- package/src/store.ts +2 -0
- package/src/types.ts +9 -0
- package/test/parity_media_download.test.ts +100 -0
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
<p style="text-align: center;">
|
|
7
7
|
Mock-Server that simulates being google-drive.<br />
|
|
8
|
-
Used for testing the <a href="https://rxdb.info/" target="_blank">RxDB Google-Drive-Sync</a>.<br />
|
|
8
|
+
Used for testing the <a href="https://rxdb.info/replication-google-drive.html" target="_blank">RxDB Google-Drive-Sync</a>.<br />
|
|
9
|
+
Sister project to <a href="https://github.com/pubkey/microsoft-onedrive-mock" target="_blank">microsoft-onedrive-mock</a>.<br />
|
|
9
10
|
Mostly Vibe-Coded.<br />
|
|
10
11
|
</p>
|
|
11
12
|
|
package/dist/index.js
CHANGED
|
@@ -52,8 +52,17 @@ const createApp = (config = {}) => {
|
|
|
52
52
|
}
|
|
53
53
|
next();
|
|
54
54
|
}));
|
|
55
|
-
app.use(express_1.default.json(
|
|
56
|
-
|
|
55
|
+
app.use(express_1.default.json({
|
|
56
|
+
verify: (req, res, buf) => {
|
|
57
|
+
req.rawBody = buf;
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
app.use(express_1.default.text({
|
|
61
|
+
type: ['multipart/mixed', 'multipart/related', 'text/*', 'application/xml'],
|
|
62
|
+
verify: (req, res, buf) => {
|
|
63
|
+
req.rawBody = buf;
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
57
66
|
// Batch Route
|
|
58
67
|
app.post('/batch', batch_1.handleBatchRequest);
|
|
59
68
|
app.post('/batch/drive/v3', batch_1.handleBatchRequest);
|
package/dist/routes/v3.js
CHANGED
|
@@ -248,16 +248,18 @@ const createV3Router = () => {
|
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
250
|
if (uploadType === 'media') {
|
|
251
|
-
|
|
251
|
+
// Use rawBody if available to preserve exact content (whitespace, etc.)
|
|
252
|
+
const content = req.rawBody !== undefined ? req.rawBody : req.body;
|
|
252
253
|
// Handle edge case where express.json() parses empty body as {}
|
|
253
|
-
|
|
254
|
+
// If rawBody is used, this check might need adjustment or be irrelevant if rawBody is buffer
|
|
255
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(req.body) === '{}' && !req.rawBody) {
|
|
254
256
|
// Empty body
|
|
255
257
|
}
|
|
256
258
|
const newFile = store_1.driveStore.createFile({
|
|
257
259
|
name: "Untitled",
|
|
258
260
|
mimeType: req.headers['content-type'] || "application/octet-stream",
|
|
259
261
|
parents: [],
|
|
260
|
-
content: typeof
|
|
262
|
+
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content)
|
|
261
263
|
});
|
|
262
264
|
res.status(200).json(newFile);
|
|
263
265
|
return;
|
|
@@ -339,8 +341,10 @@ const createV3Router = () => {
|
|
|
339
341
|
if (uploadType === 'media') {
|
|
340
342
|
const rawBody = req.body;
|
|
341
343
|
// V3 update content via media upload
|
|
344
|
+
// Use rawBody if available
|
|
345
|
+
const content = req.rawBody !== undefined ? req.rawBody : rawBody;
|
|
342
346
|
const updatedFile = store_1.driveStore.updateFile(fileId, {
|
|
343
|
-
content:
|
|
347
|
+
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content),
|
|
344
348
|
modifiedTime: new Date().toISOString()
|
|
345
349
|
});
|
|
346
350
|
res.status(200).json(updatedFile);
|
|
@@ -441,7 +445,10 @@ const createV3Router = () => {
|
|
|
441
445
|
res.send("");
|
|
442
446
|
return;
|
|
443
447
|
}
|
|
444
|
-
if (
|
|
448
|
+
if (Buffer.isBuffer(file.content)) {
|
|
449
|
+
res.send(file.content);
|
|
450
|
+
}
|
|
451
|
+
else if (typeof file.content === 'object') {
|
|
445
452
|
res.json(file.content);
|
|
446
453
|
}
|
|
447
454
|
else {
|
package/dist/store.js
CHANGED
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import express from 'express';
|
|
1
|
+
import express, { Request } from 'express';
|
|
2
2
|
import cors from 'cors';
|
|
3
3
|
import { driveStore } from './store';
|
|
4
4
|
import { handleBatchRequest } from './batch';
|
|
@@ -44,8 +44,17 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
44
44
|
next();
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
app.use(express.json(
|
|
48
|
-
|
|
47
|
+
app.use(express.json({
|
|
48
|
+
verify: (req: Request, res, buf) => {
|
|
49
|
+
req.rawBody = buf;
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
app.use(express.text({
|
|
53
|
+
type: ['multipart/mixed', 'multipart/related', 'text/*', 'application/xml'],
|
|
54
|
+
verify: (req: Request, res, buf) => {
|
|
55
|
+
req.rawBody = buf;
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
49
58
|
|
|
50
59
|
// Batch Route
|
|
51
60
|
app.post('/batch', handleBatchRequest);
|
package/src/routes/v3.ts
CHANGED
|
@@ -260,9 +260,12 @@ export const createV3Router = () => {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
if (uploadType === 'media') {
|
|
263
|
-
|
|
263
|
+
// Use rawBody if available to preserve exact content (whitespace, etc.)
|
|
264
|
+
const content = req.rawBody !== undefined ? req.rawBody : req.body;
|
|
265
|
+
|
|
264
266
|
// Handle edge case where express.json() parses empty body as {}
|
|
265
|
-
|
|
267
|
+
// If rawBody is used, this check might need adjustment or be irrelevant if rawBody is buffer
|
|
268
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(req.body) === '{}' && !req.rawBody) {
|
|
266
269
|
// Empty body
|
|
267
270
|
}
|
|
268
271
|
|
|
@@ -270,7 +273,7 @@ export const createV3Router = () => {
|
|
|
270
273
|
name: "Untitled",
|
|
271
274
|
mimeType: req.headers['content-type'] || "application/octet-stream",
|
|
272
275
|
parents: [],
|
|
273
|
-
content: typeof
|
|
276
|
+
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content)
|
|
274
277
|
});
|
|
275
278
|
res.status(200).json(newFile);
|
|
276
279
|
return;
|
|
@@ -369,8 +372,11 @@ export const createV3Router = () => {
|
|
|
369
372
|
if (uploadType === 'media') {
|
|
370
373
|
const rawBody = req.body;
|
|
371
374
|
// V3 update content via media upload
|
|
375
|
+
// Use rawBody if available
|
|
376
|
+
const content = req.rawBody !== undefined ? req.rawBody : rawBody;
|
|
377
|
+
|
|
372
378
|
const updatedFile = driveStore.updateFile(fileId, {
|
|
373
|
-
content:
|
|
379
|
+
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content),
|
|
374
380
|
modifiedTime: new Date().toISOString()
|
|
375
381
|
});
|
|
376
382
|
res.status(200).json(updatedFile!);
|
|
@@ -497,7 +503,9 @@ export const createV3Router = () => {
|
|
|
497
503
|
res.send("");
|
|
498
504
|
return;
|
|
499
505
|
}
|
|
500
|
-
if (
|
|
506
|
+
if (Buffer.isBuffer(file.content)) {
|
|
507
|
+
res.send(file.content);
|
|
508
|
+
} else if (typeof file.content === 'object') {
|
|
501
509
|
res.json(file.content);
|
|
502
510
|
} else {
|
|
503
511
|
res.send(file.content);
|
package/src/store.ts
CHANGED
|
@@ -54,6 +54,8 @@ export class DriveStore {
|
|
|
54
54
|
let buffer: Buffer;
|
|
55
55
|
if (typeof content === 'string') {
|
|
56
56
|
buffer = Buffer.from(content);
|
|
57
|
+
} else if (Buffer.isBuffer(content)) {
|
|
58
|
+
buffer = content;
|
|
57
59
|
} else if (content === undefined || content === null) {
|
|
58
60
|
buffer = Buffer.from('');
|
|
59
61
|
} else {
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
3
|
+
import { getTestConfig, TestConfig } from './config';
|
|
4
|
+
import { Server } from 'http';
|
|
5
|
+
|
|
6
|
+
async function makeRequest(
|
|
7
|
+
target: Server | string,
|
|
8
|
+
method: string,
|
|
9
|
+
path: string,
|
|
10
|
+
headers: Record<string, string>,
|
|
11
|
+
body?: unknown
|
|
12
|
+
) {
|
|
13
|
+
if (typeof target === 'string') {
|
|
14
|
+
const url = `${target}${path}`;
|
|
15
|
+
const fetchOptions: RequestInit = {
|
|
16
|
+
method: method,
|
|
17
|
+
headers: headers
|
|
18
|
+
};
|
|
19
|
+
if (body) {
|
|
20
|
+
if (typeof body === 'string') {
|
|
21
|
+
fetchOptions.body = body;
|
|
22
|
+
} else {
|
|
23
|
+
fetchOptions.body = JSON.stringify(body);
|
|
24
|
+
if (!headers['Content-Type']) {
|
|
25
|
+
headers['Content-Type'] = 'application/json';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url, fetchOptions);
|
|
31
|
+
|
|
32
|
+
// Capture headers as an array of [key, value] to preserve order if possible (though Headers object iteration order is not guaranteed to match wire order in all environments, it's worth checking)
|
|
33
|
+
const headerList: [string, string][] = [];
|
|
34
|
+
res.headers.forEach((val, key) => headerList.push([key, val]));
|
|
35
|
+
|
|
36
|
+
const resText = await res.text();
|
|
37
|
+
|
|
38
|
+
return { status: res.status, headers: headerList, body: resText, rawHeaders: res.headers };
|
|
39
|
+
} else {
|
|
40
|
+
const addr = target.address();
|
|
41
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
42
|
+
const baseUrl = `http://localhost:${port}`;
|
|
43
|
+
return makeRequest(baseUrl, method, path, headers, body);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('Parity: Media Download Order', () => {
|
|
48
|
+
let config: TestConfig;
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
config = await getTestConfig();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(() => {
|
|
55
|
+
if (config) config.stop();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function req(method: string, path: string, body?: unknown, customHeaders: Record<string, string> = {}) {
|
|
59
|
+
const headers = {
|
|
60
|
+
'Authorization': `Bearer ${config.token}`,
|
|
61
|
+
...customHeaders
|
|
62
|
+
};
|
|
63
|
+
return makeRequest(config.target, method, path, headers, body);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
it('should return data in the same order and format', async () => {
|
|
69
|
+
// 1. Create a file with specific content (Pretty printed JSON)
|
|
70
|
+
const fileContent = '{\n "b": 1,\n "a": 2\n}';
|
|
71
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
72
|
+
name: 'Order Test File',
|
|
73
|
+
mimeType: 'application/json'
|
|
74
|
+
});
|
|
75
|
+
expect(createRes.status).toBe(200);
|
|
76
|
+
const fileId = JSON.parse(createRes.body).id;
|
|
77
|
+
|
|
78
|
+
// Upload content
|
|
79
|
+
const updateRes = await req('PATCH', `/upload/drive/v3/files/${fileId}?uploadType=media`, fileContent, {
|
|
80
|
+
'Content-Type': 'application/json'
|
|
81
|
+
});
|
|
82
|
+
expect(updateRes.status).toBe(200);
|
|
83
|
+
|
|
84
|
+
// 2. Download with alt=media
|
|
85
|
+
const downloadUrl = `/drive/v3/files/${encodeURIComponent(fileId)}?alt=media&supportsAllDrives=true`;
|
|
86
|
+
const res = await req('GET', downloadUrl);
|
|
87
|
+
|
|
88
|
+
console.log('--- Response Headers ---');
|
|
89
|
+
console.log(JSON.stringify(res.headers, null, 2));
|
|
90
|
+
console.log('--- Response Body ---');
|
|
91
|
+
console.log(res.body);
|
|
92
|
+
|
|
93
|
+
expect(res.status).toBe(200);
|
|
94
|
+
expect(res.body).toBe(fileContent);
|
|
95
|
+
|
|
96
|
+
// Clean up
|
|
97
|
+
await req('DELETE', `/drive/v3/files/${fileId}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
});
|