google-drive-mock 1.0.8 → 1.0.10
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/mappers.js +2 -0
- package/dist/routes/v2.js +8 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.js +60 -2
- package/package.json +2 -2
- package/scripts/check-token.ts +75 -0
- package/src/mappers.ts +2 -0
- package/src/routes/v2.ts +9 -0
- package/src/store.ts +34 -0
- package/test/mime_types.test.ts +1 -1
- package/test/missing_fields.test.ts +123 -0
- package/test/v2_upload.test.ts +99 -0
package/dist/mappers.js
CHANGED
package/dist/routes/v2.js
CHANGED
|
@@ -391,6 +391,14 @@ const createV2Router = (config) => {
|
|
|
391
391
|
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
392
392
|
return;
|
|
393
393
|
}
|
|
394
|
+
const ifMatchHeader = req.headers['if-match'];
|
|
395
|
+
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
396
|
+
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
397
|
+
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
398
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
394
402
|
const uploadType = req.query.uploadType;
|
|
395
403
|
if (uploadType === 'media') {
|
|
396
404
|
const rawBody = req.body;
|
package/dist/store.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface DriveFile {
|
|
|
9
9
|
trashed: boolean;
|
|
10
10
|
createdTime: string;
|
|
11
11
|
modifiedTime: string;
|
|
12
|
+
size: string;
|
|
13
|
+
md5Checksum: string;
|
|
12
14
|
[key: string]: unknown;
|
|
13
15
|
}
|
|
14
16
|
export interface DriveChange {
|
|
@@ -38,6 +40,7 @@ export declare class DriveStore {
|
|
|
38
40
|
private files;
|
|
39
41
|
private changes;
|
|
40
42
|
constructor();
|
|
43
|
+
private calculateStats;
|
|
41
44
|
createFile(file: Partial<DriveFile> & {
|
|
42
45
|
name: string;
|
|
43
46
|
}): DriveFile;
|
package/dist/store.js
CHANGED
|
@@ -1,18 +1,71 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.driveStore = exports.DriveStore = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
4
38
|
class DriveStore {
|
|
5
39
|
constructor() {
|
|
6
40
|
this.files = new Map();
|
|
7
41
|
this.changes = [];
|
|
8
42
|
}
|
|
43
|
+
calculateStats(content) {
|
|
44
|
+
let buffer;
|
|
45
|
+
if (typeof content === 'string') {
|
|
46
|
+
buffer = Buffer.from(content);
|
|
47
|
+
}
|
|
48
|
+
else if (content === undefined || content === null) {
|
|
49
|
+
buffer = Buffer.from('');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
buffer = Buffer.from(JSON.stringify(content));
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
size: String(buffer.length),
|
|
56
|
+
md5Checksum: crypto.createHash('md5').update(buffer).digest('hex')
|
|
57
|
+
};
|
|
58
|
+
}
|
|
9
59
|
createFile(file) {
|
|
10
60
|
if (!file.name) {
|
|
11
61
|
throw new Error("File name is required");
|
|
12
62
|
}
|
|
13
63
|
const id = file.id || Math.random().toString(36).substring(7);
|
|
14
64
|
const now = new Date().toISOString();
|
|
15
|
-
const
|
|
65
|
+
const stats = this.calculateStats(file.content);
|
|
66
|
+
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",
|
|
67
|
+
// Ensure calculated stats override provided ones
|
|
68
|
+
size: stats.size, md5Checksum: stats.md5Checksum });
|
|
16
69
|
this.files.set(id, newFile);
|
|
17
70
|
this.addChange(newFile);
|
|
18
71
|
return newFile;
|
|
@@ -21,9 +74,14 @@ class DriveStore {
|
|
|
21
74
|
const file = this.files.get(id);
|
|
22
75
|
if (!file)
|
|
23
76
|
return null;
|
|
77
|
+
// If content is being updated, recalculate stats
|
|
78
|
+
let statsUpdates = {};
|
|
79
|
+
if (updates.content !== undefined) {
|
|
80
|
+
statsUpdates = this.calculateStats(updates.content);
|
|
81
|
+
}
|
|
24
82
|
// Merge updates and increment version
|
|
25
83
|
const newVersion = file.version + 1;
|
|
26
|
-
const updatedFile = Object.assign(Object.assign(Object.assign({}, file), updates), { version: newVersion, etag: String(newVersion), modifiedTime: new Date().toISOString() });
|
|
84
|
+
const updatedFile = Object.assign(Object.assign(Object.assign(Object.assign({}, file), updates), statsUpdates), { version: newVersion, etag: String(newVersion), modifiedTime: new Date().toISOString() });
|
|
27
85
|
this.files.set(id, updatedFile);
|
|
28
86
|
this.addChange(updatedFile);
|
|
29
87
|
return updatedFile;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-drive-mock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
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",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test:slow": "LATENCY=20 vitest run",
|
|
13
13
|
"test:browser": "start-server-and-test dev http://localhost:3000 'TEST_TARGET=mock BROWSER_ENABLED=true vitest run --browser --no-file-parallelism'",
|
|
14
14
|
"test:browser:real": "TEST_TARGET=real BROWSER_ENABLED=true vitest run --browser",
|
|
15
|
-
"test:real": "TEST_TARGET=real vitest run",
|
|
15
|
+
"test:real": "ts-node scripts/check-token.ts && TEST_TARGET=real vitest run",
|
|
16
16
|
"example:login": "ts-node examples/serve-login.ts",
|
|
17
17
|
"lint": "eslint .",
|
|
18
18
|
"lint:fix": "eslint . --fix"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as https from 'https';
|
|
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
|
+
const envPath = path.resolve(__dirname, '../.ENV');
|
|
8
|
+
if (fs.existsSync(envPath)) {
|
|
9
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
10
|
+
envContent.split('\n').forEach(line => {
|
|
11
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
12
|
+
if (match) {
|
|
13
|
+
const key = match[1].trim();
|
|
14
|
+
const value = match[2].trim().replace(/^['"]|['"]$/g, ''); // strip quotes
|
|
15
|
+
if (!process.env[key]) {
|
|
16
|
+
process.env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const token = process.env.GDRIVE_TOKEN;
|
|
23
|
+
|
|
24
|
+
if (!token) {
|
|
25
|
+
console.error('❌ Error: GDRIVE_TOKEN not found in environment or .ENV file.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
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...');
|
|
33
|
+
|
|
34
|
+
const options = {
|
|
35
|
+
hostname: 'www.googleapis.com',
|
|
36
|
+
path: '/drive/v3/about?fields=user',
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': `Bearer ${token}`,
|
|
40
|
+
'User-Agent': 'node-script'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const req = https.request(options, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
|
|
47
|
+
res.on('data', (chunk) => {
|
|
48
|
+
data += chunk;
|
|
49
|
+
});
|
|
50
|
+
|
|
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);
|
|
61
|
+
}
|
|
62
|
+
} 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);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.on('error', (e) => {
|
|
71
|
+
console.error(`❌ Request error: ${e.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
req.end();
|
package/src/mappers.ts
CHANGED
|
@@ -28,6 +28,8 @@ export function toV2File(file: DriveFile): Record<string, unknown> {
|
|
|
28
28
|
isRoot: false // Mock simplification
|
|
29
29
|
})),
|
|
30
30
|
version: file.version,
|
|
31
|
+
fileSize: file.size,
|
|
32
|
+
md5Checksum: file.md5Checksum,
|
|
31
33
|
downloadUrl: `http://localhost/drive/v2/files/${file.id}?alt=media`
|
|
32
34
|
};
|
|
33
35
|
}
|
package/src/routes/v2.ts
CHANGED
|
@@ -454,6 +454,15 @@ export const createV2Router = (config: AppConfig) => {
|
|
|
454
454
|
return;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
const ifMatchHeader = req.headers['if-match'];
|
|
458
|
+
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
459
|
+
if (ifMatch && ifMatch !== '*' && ifMatch !== existingFile.etag) {
|
|
460
|
+
if (ifMatch !== existingFile.etag && ifMatch !== `"${existingFile.etag}"`) {
|
|
461
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
457
466
|
const uploadType = req.query.uploadType as string;
|
|
458
467
|
|
|
459
468
|
if (uploadType === 'media') {
|
package/src/store.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
|
|
1
3
|
export interface DriveFile {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
@@ -9,6 +11,8 @@ export interface DriveFile {
|
|
|
9
11
|
trashed: boolean;
|
|
10
12
|
createdTime: string;
|
|
11
13
|
modifiedTime: string;
|
|
14
|
+
size: string;
|
|
15
|
+
md5Checksum: string;
|
|
12
16
|
[key: string]: unknown;
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -46,22 +50,45 @@ export class DriveStore {
|
|
|
46
50
|
this.changes = [];
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
private calculateStats(content: unknown): { size: string, md5Checksum: string } {
|
|
54
|
+
let buffer: Buffer;
|
|
55
|
+
if (typeof content === 'string') {
|
|
56
|
+
buffer = Buffer.from(content);
|
|
57
|
+
} else if (content === undefined || content === null) {
|
|
58
|
+
buffer = Buffer.from('');
|
|
59
|
+
} else {
|
|
60
|
+
buffer = Buffer.from(JSON.stringify(content));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
size: String(buffer.length),
|
|
65
|
+
md5Checksum: crypto.createHash('md5').update(buffer).digest('hex')
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
createFile(file: Partial<DriveFile> & { name: string }): DriveFile {
|
|
50
70
|
if (!file.name) {
|
|
51
71
|
throw new Error("File name is required");
|
|
52
72
|
}
|
|
53
73
|
const id = file.id || Math.random().toString(36).substring(7);
|
|
54
74
|
const now = new Date().toISOString();
|
|
75
|
+
|
|
76
|
+
const stats = this.calculateStats(file.content);
|
|
77
|
+
|
|
55
78
|
const newFile: DriveFile = {
|
|
56
79
|
kind: "drive#file",
|
|
57
80
|
mimeType: "application/octet-stream",
|
|
58
81
|
trashed: false,
|
|
59
82
|
createdTime: now,
|
|
60
83
|
modifiedTime: now,
|
|
84
|
+
|
|
61
85
|
...file,
|
|
62
86
|
id,
|
|
63
87
|
version: 1, // Initialize version
|
|
64
88
|
etag: "1", // Initialize etag
|
|
89
|
+
// Ensure calculated stats override provided ones
|
|
90
|
+
size: stats.size,
|
|
91
|
+
md5Checksum: stats.md5Checksum
|
|
65
92
|
};
|
|
66
93
|
|
|
67
94
|
this.files.set(id, newFile);
|
|
@@ -73,11 +100,18 @@ export class DriveStore {
|
|
|
73
100
|
const file = this.files.get(id);
|
|
74
101
|
if (!file) return null;
|
|
75
102
|
|
|
103
|
+
// If content is being updated, recalculate stats
|
|
104
|
+
let statsUpdates = {};
|
|
105
|
+
if (updates.content !== undefined) {
|
|
106
|
+
statsUpdates = this.calculateStats(updates.content);
|
|
107
|
+
}
|
|
108
|
+
|
|
76
109
|
// Merge updates and increment version
|
|
77
110
|
const newVersion = file.version + 1;
|
|
78
111
|
const updatedFile = {
|
|
79
112
|
...file,
|
|
80
113
|
...updates,
|
|
114
|
+
...statsUpdates,
|
|
81
115
|
version: newVersion,
|
|
82
116
|
etag: String(newVersion),
|
|
83
117
|
modifiedTime: new Date().toISOString()
|
package/test/mime_types.test.ts
CHANGED
|
@@ -54,7 +54,7 @@ describe('MIME Type Handling', () => {
|
|
|
54
54
|
expect(await getRes.json()).toEqual(content);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it('should update MIME type and return new Content-Type header', async () => {
|
|
57
|
+
it('should update MIME type and return new Content-Type header', { timeout: 10000 }, async () => {
|
|
58
58
|
// Create as text/plain
|
|
59
59
|
const createRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
|
|
60
60
|
method: 'POST',
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { getTestConfig, TestConfig } from './config';
|
|
3
|
+
|
|
4
|
+
describe('Missing Fields Support (Size & MD5)', () => {
|
|
5
|
+
let config: TestConfig;
|
|
6
|
+
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
config = await getTestConfig();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterAll(() => {
|
|
12
|
+
if (config) config.stop();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return size and md5Checksum in V3 GET', async () => {
|
|
16
|
+
const content = JSON.stringify({ foo: 'bar' });
|
|
17
|
+
const metadata = { name: 'V3 Size Test', mimeType: 'application/json' };
|
|
18
|
+
|
|
19
|
+
const boundary = 'foo_bar_baz';
|
|
20
|
+
const delimiter = `\r\n--${boundary}\r\n`;
|
|
21
|
+
const closeDelim = `\r\n--${boundary}--`;
|
|
22
|
+
|
|
23
|
+
const body = delimiter +
|
|
24
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
25
|
+
JSON.stringify(metadata) +
|
|
26
|
+
delimiter +
|
|
27
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
28
|
+
content +
|
|
29
|
+
closeDelim;
|
|
30
|
+
|
|
31
|
+
const createRes = await fetch(`${config.baseUrl}/upload/drive/v3/files?uploadType=multipart`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': `Bearer ${config.token}`,
|
|
35
|
+
'Content-Type': `multipart/related; boundary="${boundary}"`
|
|
36
|
+
},
|
|
37
|
+
body: body
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(createRes.status).toBe(200);
|
|
41
|
+
const file = await createRes.json();
|
|
42
|
+
|
|
43
|
+
const fields = 'id,name,size,md5Checksum';
|
|
44
|
+
const getRes = await fetch(`${config.baseUrl}/drive/v3/files/${file.id}?fields=${fields}`, {
|
|
45
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
46
|
+
});
|
|
47
|
+
expect(getRes.status).toBe(200);
|
|
48
|
+
const getFile = await getRes.json();
|
|
49
|
+
|
|
50
|
+
expect(getFile.size).toBeDefined();
|
|
51
|
+
// size should be string in V3
|
|
52
|
+
expect(String(getFile.size)).toBe(String(content.length));
|
|
53
|
+
expect(getFile.md5Checksum).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return fileSize and md5Checksum in V2 GET', async () => {
|
|
57
|
+
const content = 'V2 Content';
|
|
58
|
+
|
|
59
|
+
// Use simple upload for V2 to ensure content and metadata
|
|
60
|
+
const uploadRes = await fetch(`${config.baseUrl}/upload/drive/v2/files?uploadType=media`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'Authorization': `Bearer ${config.token}`,
|
|
64
|
+
'Content-Type': 'text/plain'
|
|
65
|
+
},
|
|
66
|
+
body: content
|
|
67
|
+
});
|
|
68
|
+
expect(uploadRes.status).toBe(200);
|
|
69
|
+
const file = await uploadRes.json();
|
|
70
|
+
|
|
71
|
+
expect(file.fileSize).toBeDefined(); // V2 uses fileSize
|
|
72
|
+
expect(Number(file.fileSize)).toBe(content.length);
|
|
73
|
+
expect(file.md5Checksum).toBeDefined();
|
|
74
|
+
|
|
75
|
+
// Verify GET
|
|
76
|
+
const getRes = await fetch(`${config.baseUrl}/drive/v2/files/${file.id}`, {
|
|
77
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
78
|
+
});
|
|
79
|
+
const getFile = await getRes.json();
|
|
80
|
+
expect(getFile.fileSize).toBeDefined();
|
|
81
|
+
expect(Number(getFile.fileSize)).toBe(content.length);
|
|
82
|
+
expect(getFile.md5Checksum).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return correct size and md5Checksum for EMPTY file', async () => {
|
|
86
|
+
const content = '';
|
|
87
|
+
const md5Empty = 'd41d8cd98f00b204e9800998ecf8427e'; // MD5 of empty string
|
|
88
|
+
|
|
89
|
+
// Upload empty file via V2 media upload
|
|
90
|
+
const uploadRes = await fetch(`${config.baseUrl}/upload/drive/v2/files?uploadType=media`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Authorization': `Bearer ${config.token}`,
|
|
94
|
+
'Content-Type': 'text/plain'
|
|
95
|
+
},
|
|
96
|
+
body: content
|
|
97
|
+
});
|
|
98
|
+
expect(uploadRes.status).toBe(200);
|
|
99
|
+
const file = await uploadRes.json();
|
|
100
|
+
|
|
101
|
+
// Check response
|
|
102
|
+
expect(file.fileSize).toBeDefined();
|
|
103
|
+
expect(Number(file.fileSize)).toBe(0);
|
|
104
|
+
expect(file.md5Checksum).toBe(md5Empty);
|
|
105
|
+
|
|
106
|
+
// Verify GET V2
|
|
107
|
+
const getV2 = await fetch(`${config.baseUrl}/drive/v2/files/${file.id}`, {
|
|
108
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
109
|
+
});
|
|
110
|
+
const v2File = await getV2.json();
|
|
111
|
+
expect(Number(v2File.fileSize)).toBe(0);
|
|
112
|
+
expect(v2File.md5Checksum).toBe(md5Empty);
|
|
113
|
+
|
|
114
|
+
// Verify GET V3
|
|
115
|
+
const fields = 'id,name,size,md5Checksum';
|
|
116
|
+
const getV3 = await fetch(`${config.baseUrl}/drive/v3/files/${file.id}?fields=${fields}`, {
|
|
117
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
118
|
+
});
|
|
119
|
+
const v3File = await getV3.json();
|
|
120
|
+
expect(Number(v3File.size)).toBe(0);
|
|
121
|
+
expect(v3File.md5Checksum).toBe(md5Empty);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/test/v2_upload.test.ts
CHANGED
|
@@ -139,4 +139,103 @@ describe('V2 Upload Features', () => {
|
|
|
139
139
|
});
|
|
140
140
|
expect(await contentRes.text()).toBe(newContent);
|
|
141
141
|
});
|
|
142
|
+
it('should respect If-Match header in V2 media upload (PUT)', async () => {
|
|
143
|
+
// 1. Create file
|
|
144
|
+
const createRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {
|
|
147
|
+
'Authorization': `Bearer ${config.token}`,
|
|
148
|
+
'Content-Type': 'application/json'
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({ title: 'ETag Media Test', mimeType: 'text/plain' })
|
|
151
|
+
});
|
|
152
|
+
const file = await createRes.json();
|
|
153
|
+
const fileId = file.id;
|
|
154
|
+
const etag = file.etag;
|
|
155
|
+
|
|
156
|
+
const invalidEtag = etag ? etag.replace(/.$/, '0') : '"wrong-etag"';
|
|
157
|
+
|
|
158
|
+
// 2. Update with WRONG ETag
|
|
159
|
+
const wrongEtagRes = await fetch(`${config.baseUrl}/upload/drive/v2/files/${fileId}?uploadType=media`, {
|
|
160
|
+
method: 'PUT',
|
|
161
|
+
headers: {
|
|
162
|
+
'Authorization': `Bearer ${config.token}`,
|
|
163
|
+
'Content-Type': 'text/plain',
|
|
164
|
+
'If-Match': invalidEtag
|
|
165
|
+
},
|
|
166
|
+
body: 'Should Fail'
|
|
167
|
+
});
|
|
168
|
+
expect(wrongEtagRes.status).toBe(412);
|
|
169
|
+
|
|
170
|
+
// 3. Update with CORRECT ETag
|
|
171
|
+
const correctEtagRes = await fetch(`${config.baseUrl}/upload/drive/v2/files/${fileId}?uploadType=media`, {
|
|
172
|
+
method: 'PUT',
|
|
173
|
+
headers: {
|
|
174
|
+
'Authorization': `Bearer ${config.token}`,
|
|
175
|
+
'Content-Type': 'text/plain',
|
|
176
|
+
'If-Match': etag
|
|
177
|
+
},
|
|
178
|
+
body: 'Updated with ETag'
|
|
179
|
+
});
|
|
180
|
+
expect(correctEtagRes.status).toBe(200);
|
|
181
|
+
|
|
182
|
+
// Verify Content
|
|
183
|
+
const contentRes = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}?alt=media`, {
|
|
184
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
185
|
+
});
|
|
186
|
+
expect(await contentRes.text()).toBe('Updated with ETag');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should respect If-Match header in V2 multipart upload (PUT)', async () => {
|
|
190
|
+
// 1. Create file
|
|
191
|
+
const createRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: {
|
|
194
|
+
'Authorization': `Bearer ${config.token}`,
|
|
195
|
+
'Content-Type': 'application/json'
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({ title: 'ETag Multipart Test', mimeType: 'text/plain' })
|
|
198
|
+
});
|
|
199
|
+
const file = await createRes.json();
|
|
200
|
+
const fileId = file.id;
|
|
201
|
+
const etag = file.etag;
|
|
202
|
+
|
|
203
|
+
const boundary = 'foo_bar_baz';
|
|
204
|
+
const delimiter = `\r\n--${boundary}\r\n`;
|
|
205
|
+
const closeDelim = `\r\n--${boundary}--`;
|
|
206
|
+
const body = delimiter +
|
|
207
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
208
|
+
JSON.stringify({ title: 'Updated Title' }) +
|
|
209
|
+
delimiter +
|
|
210
|
+
'Content-Type: text/plain\r\n\r\n' +
|
|
211
|
+
'Multipart Update' +
|
|
212
|
+
closeDelim;
|
|
213
|
+
|
|
214
|
+
const invalidEtag = etag ? etag.replace(/.$/, '0') : '"wrong-etag"';
|
|
215
|
+
|
|
216
|
+
// 2. Update with WRONG ETag
|
|
217
|
+
const wrongEtagRes = await fetch(`${config.baseUrl}/upload/drive/v2/files/${fileId}?uploadType=multipart`, {
|
|
218
|
+
method: 'PUT',
|
|
219
|
+
headers: {
|
|
220
|
+
'Authorization': `Bearer ${config.token}`,
|
|
221
|
+
'Content-Type': `multipart/related; boundary="${boundary}"`,
|
|
222
|
+
'If-Match': invalidEtag
|
|
223
|
+
},
|
|
224
|
+
body: body
|
|
225
|
+
});
|
|
226
|
+
expect(wrongEtagRes.status).toBe(412);
|
|
227
|
+
|
|
228
|
+
// 3. Update with CORRECT ETag
|
|
229
|
+
const correctEtagRes = await fetch(`${config.baseUrl}/upload/drive/v2/files/${fileId}?uploadType=multipart`, {
|
|
230
|
+
method: 'PUT',
|
|
231
|
+
headers: {
|
|
232
|
+
'Authorization': `Bearer ${config.token}`,
|
|
233
|
+
'Content-Type': `multipart/related; boundary="${boundary}"`,
|
|
234
|
+
'If-Match': etag
|
|
235
|
+
},
|
|
236
|
+
body: body
|
|
237
|
+
});
|
|
238
|
+
expect(correctEtagRes.status).toBe(200);
|
|
239
|
+
expect((await correctEtagRes.json()).title).toBe('Updated Title');
|
|
240
|
+
});
|
|
142
241
|
});
|