google-drive-mock 1.0.9 → 1.0.11
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 +3 -2
- package/dist/mappers.js +2 -0
- package/dist/routes/v2.js +17 -2
- package/dist/routes/v3.js +139 -67
- 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 +20 -2
- package/src/routes/v3.ts +153 -68
- package/src/store.ts +34 -0
- package/test/batch_and_query.test.ts +206 -0
- package/test/dates_and_sorting.test.ts +485 -0
- package/test/mime_types.test.ts +1 -1
- package/test/missing_fields.test.ts +125 -0
- package/test/multipart_behavior.test.ts +436 -0
- package/test/v2_content.test.ts +184 -0
package/src/routes/v3.ts
CHANGED
|
@@ -21,55 +21,67 @@ export const createV3Router = () => {
|
|
|
21
21
|
|
|
22
22
|
if (q) {
|
|
23
23
|
// Enhanced query parser for Mock
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const orParts = q.split(' or ');
|
|
26
25
|
files = files.filter(file => {
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
26
|
+
return orParts.some(orPart => {
|
|
27
|
+
const andParts = orPart.split(' and ').map(p => p.trim());
|
|
28
|
+
return andParts.every(part => {
|
|
29
|
+
// name = '...'
|
|
30
|
+
if (part.startsWith("name = '")) {
|
|
31
|
+
const name = part.match(/name = '(.*)'/)?.[1];
|
|
32
|
+
// Handle escaped quotes if simple match fails, but simple regex here might be enough for now
|
|
33
|
+
// The user does name.replace("'", "\\'") but the regex (.*) is greedy and might consume escaped quotes correctly-ish
|
|
34
|
+
// but we are stripping the outer quotes.
|
|
35
|
+
// If user sends: name = 'foo\'bar', regex captures: foo\'bar
|
|
36
|
+
// We need to unescape? The user sends escaped string for the Query Parser.
|
|
37
|
+
// Drive API expects the string inside quotes to be the value.
|
|
38
|
+
// If `name = 'a\'b'`, the value is `a'b`.
|
|
39
|
+
// JSON.parse(`"${name}"`) might decode it if we treat it as JSON string? Use simple replace for now.
|
|
40
|
+
const finalName = name ? name.replace(/\\'/g, "'") : name;
|
|
41
|
+
return file.name === finalName;
|
|
42
|
+
}
|
|
43
|
+
// name contains '...'
|
|
44
|
+
if (part.startsWith("name contains '")) {
|
|
45
|
+
const token = part.match(/name contains '(.*)'/)?.[1];
|
|
46
|
+
const finalToken = token ? token.replace(/\\'/g, "'") : token;
|
|
47
|
+
return finalToken && file.name.includes(finalToken);
|
|
48
|
+
}
|
|
49
|
+
// 'ID' in parents
|
|
50
|
+
if (part.includes(" in parents")) {
|
|
51
|
+
const parentId = part.match(/'(.*)' in parents/)?.[1];
|
|
52
|
+
return parentId && file.parents?.includes(parentId);
|
|
53
|
+
}
|
|
54
|
+
// trashed = ...
|
|
55
|
+
if (part === "trashed = false") {
|
|
56
|
+
return file.trashed !== true;
|
|
57
|
+
}
|
|
58
|
+
if (part === "trashed = true") {
|
|
59
|
+
return file.trashed === true;
|
|
60
|
+
}
|
|
61
|
+
// mimeType = '...'
|
|
62
|
+
if (part.startsWith("mimeType = '")) {
|
|
63
|
+
const mime = part.match(/mimeType = '(.*)'/)?.[1];
|
|
64
|
+
return file.mimeType === mime;
|
|
65
|
+
}
|
|
66
|
+
// mimeType != '...'
|
|
67
|
+
if (part.startsWith("mimeType != '")) {
|
|
68
|
+
const mime = part.match(/mimeType != '(.*)'/)?.[1];
|
|
69
|
+
return file.mimeType !== mime;
|
|
70
|
+
}
|
|
71
|
+
// modifiedTime > '...'
|
|
72
|
+
if (part.startsWith("modifiedTime > '")) {
|
|
73
|
+
const timeStr = part.match(/modifiedTime > '(.*)'/)?.[1];
|
|
74
|
+
return timeStr && new Date(file.modifiedTime) > new Date(timeStr);
|
|
75
|
+
}
|
|
76
|
+
// modifiedTime < '...'
|
|
77
|
+
if (part.startsWith("modifiedTime < '")) {
|
|
78
|
+
const timeStr = part.match(/modifiedTime < '(.*)'/)?.[1];
|
|
79
|
+
return timeStr && new Date(file.modifiedTime) < new Date(timeStr);
|
|
80
|
+
}
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
// Ignore unknown filters for now
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
73
85
|
});
|
|
74
86
|
});
|
|
75
87
|
}
|
|
@@ -143,8 +155,25 @@ export const createV3Router = () => {
|
|
|
143
155
|
// Upload Files Route
|
|
144
156
|
app.post('/upload/drive/v3/files', (req: Request, res: Response) => {
|
|
145
157
|
const uploadType = req.query.uploadType;
|
|
146
|
-
if (uploadType !== 'multipart') {
|
|
147
|
-
res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart is supported in this mock route" } });
|
|
158
|
+
if (uploadType !== 'multipart' && uploadType !== 'media') {
|
|
159
|
+
res.status(400).json({ error: { code: 400, message: "Only uploadType=multipart or uploadType=media is supported in this mock route" } });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (uploadType === 'media') {
|
|
164
|
+
const rawBody = req.body;
|
|
165
|
+
// Handle edge case where express.json() parses empty body as {}
|
|
166
|
+
if (req.headers['content-length'] === '0' && JSON.stringify(rawBody) === '{}') {
|
|
167
|
+
// Empty body
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const newFile = driveStore.createFile({
|
|
171
|
+
name: "Untitled",
|
|
172
|
+
mimeType: req.headers['content-type'] || "application/octet-stream",
|
|
173
|
+
parents: [],
|
|
174
|
+
content: typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody) // Handle body if parsed
|
|
175
|
+
});
|
|
176
|
+
res.status(200).json(newFile);
|
|
148
177
|
return;
|
|
149
178
|
}
|
|
150
179
|
|
|
@@ -214,23 +243,6 @@ export const createV3Router = () => {
|
|
|
214
243
|
content = contentPart.body;
|
|
215
244
|
}
|
|
216
245
|
|
|
217
|
-
const existing = driveStore.listFiles().find(f => {
|
|
218
|
-
if (f.name !== metadata.name) return false;
|
|
219
|
-
// Filter trashed?
|
|
220
|
-
if (f.trashed) return false;
|
|
221
|
-
|
|
222
|
-
const newParents = metadata.parents || [];
|
|
223
|
-
const existingParents = f.parents || [];
|
|
224
|
-
|
|
225
|
-
if (newParents.length === 0 && existingParents.length === 0) return true;
|
|
226
|
-
return newParents.some((p: string) => existingParents.includes(p));
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (existing) {
|
|
230
|
-
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
246
|
const newFile = driveStore.createFile({
|
|
235
247
|
...metadata,
|
|
236
248
|
content: content
|
|
@@ -266,8 +278,81 @@ export const createV3Router = () => {
|
|
|
266
278
|
return;
|
|
267
279
|
}
|
|
268
280
|
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
const contentTypeHeader = req.headers['content-type'];
|
|
282
|
+
const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader;
|
|
283
|
+
|
|
284
|
+
// Check for multipart
|
|
285
|
+
if (contentType && contentType.includes('multipart/related')) {
|
|
286
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/);
|
|
287
|
+
if (!boundaryMatch) {
|
|
288
|
+
res.status(400).json({ error: { code: 400, message: "Multipart boundary missing" } });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
let boundary = boundaryMatch[1];
|
|
292
|
+
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
293
|
+
boundary = boundary.substring(1, boundary.length - 1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const rawBody = req.body;
|
|
297
|
+
if (typeof rawBody !== 'string') {
|
|
298
|
+
res.status(400).json({ error: { code: 400, message: "Body parsing failed" } });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const parts = rawBody.split(`--${boundary}`);
|
|
303
|
+
const validParts = parts.filter(p => p.trim() !== '' && p.trim() !== '--');
|
|
304
|
+
|
|
305
|
+
if (validParts.length < 2) {
|
|
306
|
+
res.status(400).json({ error: { code: 400, message: "Invalid multipart body: expected at least metadata and content" } });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const parsePart = (rawPart: string) => {
|
|
311
|
+
const splitIndex = rawPart.indexOf('\r\n\r\n');
|
|
312
|
+
if (splitIndex === -1) return null;
|
|
313
|
+
const headers = rawPart.substring(0, splitIndex).trim();
|
|
314
|
+
const body = rawPart.substring(splitIndex + 4);
|
|
315
|
+
return {
|
|
316
|
+
headers,
|
|
317
|
+
body: body.replace(/\r\n$/, '') // Remove trailing CRLF from part body
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const metadataPart = parsePart(validParts[0]);
|
|
322
|
+
const contentPart = parsePart(validParts[1]);
|
|
323
|
+
|
|
324
|
+
if (!metadataPart || !contentPart) {
|
|
325
|
+
res.status(400).json({ error: { code: 400, message: "Failed to parse parts" } });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let metadata;
|
|
330
|
+
try {
|
|
331
|
+
metadata = JSON.parse(metadataPart.body);
|
|
332
|
+
} catch {
|
|
333
|
+
res.status(400).json({ error: { code: 400, message: "Invalid JSON in metadata part" } });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let content;
|
|
338
|
+
try {
|
|
339
|
+
content = JSON.parse(contentPart.body);
|
|
340
|
+
} catch {
|
|
341
|
+
content = contentPart.body;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Perform update
|
|
345
|
+
const updatedFile = driveStore.updateFile(fileId, {
|
|
346
|
+
...metadata,
|
|
347
|
+
content: content,
|
|
348
|
+
modifiedTime: new Date().toISOString()
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
res.status(200).json(updatedFile);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
res.status(400).json({ error: { code: 400, message: "Only uploadType=media or multipart/related is supported for V3 PATCH upload" } });
|
|
271
356
|
});
|
|
272
357
|
|
|
273
358
|
// Files: Create (Standard)
|
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()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
3
|
+
import { getTestConfig, TestConfig } from './config';
|
|
4
|
+
|
|
5
|
+
describe('Batch and Complex Query Operations', () => {
|
|
6
|
+
let config: TestConfig;
|
|
7
|
+
let createdFileIds: string[] = [];
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
config = await getTestConfig();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
// Cleanup if needed
|
|
15
|
+
if (config) config.stop();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should perform bulk insert using batch API', async () => {
|
|
19
|
+
const docs = [
|
|
20
|
+
{ id: 'bulk1', content: '{"foo":1}' },
|
|
21
|
+
{ id: 'bulk2', content: '{"bar":2}' }
|
|
22
|
+
];
|
|
23
|
+
const boundary = "batch_" + Math.random().toString(16).slice(2);
|
|
24
|
+
|
|
25
|
+
// Ensure we have a valid folder ID. Using root or a test folder from config.
|
|
26
|
+
const targetFolderId = config.testFolderId;
|
|
27
|
+
|
|
28
|
+
const parts = docs.map((doc, i) => {
|
|
29
|
+
const id = doc.id;
|
|
30
|
+
const body = JSON.stringify({
|
|
31
|
+
name: id + '.json',
|
|
32
|
+
mimeType: 'application/json',
|
|
33
|
+
parents: [targetFolderId],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
`--${boundary}\r\n` +
|
|
38
|
+
`Content-Type: application/http\r\n` +
|
|
39
|
+
`Content-ID: <item-${i}>\r\n\r\n` +
|
|
40
|
+
`POST /drive/v3/files HTTP/1.1\r\n` +
|
|
41
|
+
`Content-Type: application/json; charset=UTF-8\r\n\r\n` +
|
|
42
|
+
`${body}\r\n`
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const batchBody = parts.join("") + `--${boundary}--`;
|
|
47
|
+
|
|
48
|
+
const url = config.baseUrl + "/batch/drive/v3";
|
|
49
|
+
console.log('Sending batch insert request to:', url);
|
|
50
|
+
|
|
51
|
+
const res = await fetch(url, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${config.token}`,
|
|
55
|
+
"Content-Type": `multipart/mixed; boundary=${boundary}`,
|
|
56
|
+
},
|
|
57
|
+
body: batchBody,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(res.status).toBe(200);
|
|
61
|
+
|
|
62
|
+
const contentType = res.headers.get('content-type');
|
|
63
|
+
expect(contentType).toContain('multipart/mixed');
|
|
64
|
+
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
console.log('Batch Insert Response:', text);
|
|
67
|
+
|
|
68
|
+
// Verify operations succeeded
|
|
69
|
+
expect(text).toContain('HTTP/1.1 200 OK');
|
|
70
|
+
|
|
71
|
+
// Extract IDs for later use in update tests
|
|
72
|
+
// This is a bit hacky parsing but sufficient for test
|
|
73
|
+
// Responses are JSON inside multipart
|
|
74
|
+
// We can list files to get IDs reliably
|
|
75
|
+
const listRes = await fetch(config.baseUrl + `/drive/v3/files?q='${targetFolderId}'+in+parents`, {
|
|
76
|
+
headers: { Authorization: `Bearer ${config.token}` }
|
|
77
|
+
});
|
|
78
|
+
const listData = await listRes.json();
|
|
79
|
+
const files = listData.files.filter((f: { name: string; id: string }) => f.name === 'bulk1.json' || f.name === 'bulk2.json');
|
|
80
|
+
expect(files.length).toBe(2);
|
|
81
|
+
createdFileIds = files.map((f: { id: string }) => f.id);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should perform bulk find using complex query', async () => {
|
|
85
|
+
// Ensure the files exist from previous test
|
|
86
|
+
expect(createdFileIds.length).toBe(2);
|
|
87
|
+
const docIds = ['bulk1', 'bulk2'];
|
|
88
|
+
|
|
89
|
+
const fileNames = docIds.map(id => id + '.json');
|
|
90
|
+
let q = fileNames
|
|
91
|
+
.map(name => `name = '${name.replace("'", "\\'")}'`)
|
|
92
|
+
.join(' or ');
|
|
93
|
+
q += ' and trashed = false';
|
|
94
|
+
q += ' and \'' + config.testFolderId + '\' in parents';
|
|
95
|
+
|
|
96
|
+
console.log('Bulk Find Query:', q);
|
|
97
|
+
|
|
98
|
+
const params = new URLSearchParams({
|
|
99
|
+
q,
|
|
100
|
+
fields: "nextPageToken, files(id,name,mimeType,parents,modifiedTime,size)",
|
|
101
|
+
includeItemsFromAllDrives: "true",
|
|
102
|
+
supportsAllDrives: "true",
|
|
103
|
+
});
|
|
104
|
+
const url = config.baseUrl + '/drive/v3/files?' + params.toString();
|
|
105
|
+
const res = await fetch(url, {
|
|
106
|
+
method: "GET",
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${config.token}`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(res.status).toBe(200);
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
|
|
115
|
+
// Should find both files
|
|
116
|
+
expect(data.files).toBeDefined();
|
|
117
|
+
// Depending on query parsing logic, it might find one or both or none if logic is broken
|
|
118
|
+
// The expectation is that this query works "like" finding specific files in a folder
|
|
119
|
+
const foundNames = data.files.map((f: { name: string }) => f.name);
|
|
120
|
+
expect(foundNames).toContain('bulk1.json');
|
|
121
|
+
expect(foundNames).toContain('bulk2.json');
|
|
122
|
+
expect(data.files.length).toBeGreaterThanOrEqual(2);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should perform bulk update using batch API', async () => {
|
|
126
|
+
expect(createdFileIds.length).toBe(2);
|
|
127
|
+
|
|
128
|
+
interface DocUpdate {
|
|
129
|
+
id: string;
|
|
130
|
+
newName: string;
|
|
131
|
+
}
|
|
132
|
+
const docs: DocUpdate[] = [
|
|
133
|
+
{ id: 'bulk1', newName: 'bulk1_updated' },
|
|
134
|
+
{ id: 'bulk2', newName: 'bulk2_updated' }
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// Map doc ID to file ID (assuming order or searching)
|
|
138
|
+
// For simplicity, we'll fetch IDs again or use stored ones knowing names match
|
|
139
|
+
// Let's assume createdFileIds corresponds to 'bulk1.json' and 'bulk2.json' somehow
|
|
140
|
+
// But better to use exact mapping.
|
|
141
|
+
|
|
142
|
+
// Re-fetch to be sure of mapping
|
|
143
|
+
const listRes = await fetch(config.baseUrl + `/drive/v3/files?q='${config.testFolderId}'+in+parents`, {
|
|
144
|
+
headers: { Authorization: `Bearer ${config.token}` }
|
|
145
|
+
});
|
|
146
|
+
const listData = await listRes.json();
|
|
147
|
+
const fileIdByDocId: Record<string, string> = {};
|
|
148
|
+
for (const f of listData.files) {
|
|
149
|
+
if (f.name === 'bulk1.json') fileIdByDocId['bulk1'] = f.id;
|
|
150
|
+
if (f.name === 'bulk2.json') fileIdByDocId['bulk2'] = f.id;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const boundary = "batch_" + Math.random().toString(16).slice(2);
|
|
154
|
+
|
|
155
|
+
const parts = docs.map((doc, i) => {
|
|
156
|
+
const id = doc.id;
|
|
157
|
+
const fileId = fileIdByDocId[id];
|
|
158
|
+
if (!fileId) throw new Error(`File ID not found for ${id}`);
|
|
159
|
+
|
|
160
|
+
const body = JSON.stringify({
|
|
161
|
+
name: doc.newName + '.json',
|
|
162
|
+
mimeType: "application/json",
|
|
163
|
+
// parents: [config.testFolderId], // Optional in update usually
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
`--${boundary}\r\n` +
|
|
168
|
+
`Content-Type: application/http\r\n` +
|
|
169
|
+
`Content-ID: <item-${i}>\r\n\r\n` +
|
|
170
|
+
`PATCH /drive/v3/files/${encodeURIComponent(fileId)}?supportsAllDrives=true&fields=id,name,mimeType,parents HTTP/1.1\r\n` +
|
|
171
|
+
`Content-Type: application/json; charset=UTF-8\r\n\r\n` +
|
|
172
|
+
`${body}\r\n`
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const batchBody = parts.join("") + `--${boundary}--`;
|
|
177
|
+
|
|
178
|
+
const url = config.baseUrl + "/batch/drive/v3";
|
|
179
|
+
console.log('Sending batch update request to:', url);
|
|
180
|
+
|
|
181
|
+
const res = await fetch(url, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Bearer ${config.token}`,
|
|
185
|
+
"Content-Type": `multipart/mixed; boundary=${boundary}`,
|
|
186
|
+
},
|
|
187
|
+
body: batchBody,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(res.status).toBe(200);
|
|
191
|
+
const text = await res.text();
|
|
192
|
+
console.log('Batch Update Response:', text);
|
|
193
|
+
|
|
194
|
+
expect(text).toContain('HTTP/1.1 200 OK');
|
|
195
|
+
|
|
196
|
+
// Verify updates
|
|
197
|
+
const verifyRes = await fetch(config.baseUrl + `/drive/v3/files?q='${config.testFolderId}'+in+parents`, {
|
|
198
|
+
headers: { Authorization: `Bearer ${config.token}` }
|
|
199
|
+
});
|
|
200
|
+
const verifyData = await verifyRes.json();
|
|
201
|
+
const names = verifyData.files.map((f: { name: string }) => f.name);
|
|
202
|
+
expect(names).toContain('bulk1_updated.json');
|
|
203
|
+
expect(names).toContain('bulk2_updated.json');
|
|
204
|
+
expect(names).not.toContain('bulk1.json');
|
|
205
|
+
});
|
|
206
|
+
});
|