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/src/routes/v3.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express, { Request, Response } from 'express';
|
|
2
2
|
import { driveStore } from '../store';
|
|
3
|
-
import { applyFields } from '../mappers';
|
|
3
|
+
import { applyFields, toV3File } from '../mappers';
|
|
4
4
|
|
|
5
5
|
export const createV3Router = () => {
|
|
6
6
|
const app = express.Router();
|
|
@@ -202,7 +202,7 @@ export const createV3Router = () => {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
const totalFiles = files.length;
|
|
205
|
-
const resultFiles = files.slice(skip, skip + pageSize);
|
|
205
|
+
const resultFiles = files.slice(skip, skip + pageSize).map(toV3File);
|
|
206
206
|
|
|
207
207
|
let nextPageToken: string | undefined;
|
|
208
208
|
if (skip + pageSize < totalFiles) {
|
|
@@ -218,6 +218,23 @@ export const createV3Router = () => {
|
|
|
218
218
|
};
|
|
219
219
|
|
|
220
220
|
const fields = req.query.fields as string;
|
|
221
|
+
if (fields && (fields.includes('etag') || fields.includes('kind,etag'))) {
|
|
222
|
+
res.status(400).json({
|
|
223
|
+
error: {
|
|
224
|
+
code: 400,
|
|
225
|
+
message: "Invalid field selection etag",
|
|
226
|
+
errors: [{
|
|
227
|
+
message: "Invalid field selection etag",
|
|
228
|
+
domain: "global",
|
|
229
|
+
reason: "invalidParameter",
|
|
230
|
+
location: "fields",
|
|
231
|
+
locationType: "parameter"
|
|
232
|
+
}]
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
221
238
|
if (fields) {
|
|
222
239
|
res.json(applyFields(response, fields));
|
|
223
240
|
} else {
|
|
@@ -243,11 +260,20 @@ export const createV3Router = () => {
|
|
|
243
260
|
}
|
|
244
261
|
|
|
245
262
|
const result = driveStore.getChanges(pageToken);
|
|
263
|
+
const mappedChanges = result.changes.map(c => {
|
|
264
|
+
if (c.file) {
|
|
265
|
+
return {
|
|
266
|
+
...c,
|
|
267
|
+
file: toV3File(c.file)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return c;
|
|
271
|
+
});
|
|
246
272
|
res.json({
|
|
247
273
|
kind: "drive#changeList",
|
|
248
274
|
newStartPageToken: result.newStartPageToken,
|
|
249
275
|
nextPageToken: result.nextPageToken,
|
|
250
|
-
changes:
|
|
276
|
+
changes: mappedChanges
|
|
251
277
|
});
|
|
252
278
|
});
|
|
253
279
|
|
|
@@ -275,7 +301,7 @@ export const createV3Router = () => {
|
|
|
275
301
|
parents: [],
|
|
276
302
|
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content)
|
|
277
303
|
});
|
|
278
|
-
res.status(200).json(newFile);
|
|
304
|
+
res.status(200).json(toV3File(newFile));
|
|
279
305
|
return;
|
|
280
306
|
}
|
|
281
307
|
|
|
@@ -350,7 +376,7 @@ export const createV3Router = () => {
|
|
|
350
376
|
content: content
|
|
351
377
|
});
|
|
352
378
|
|
|
353
|
-
res.status(200).json(newFile);
|
|
379
|
+
res.status(200).json(toV3File(newFile));
|
|
354
380
|
});
|
|
355
381
|
|
|
356
382
|
// Upload Files: Update (PATCH)
|
|
@@ -367,6 +393,17 @@ export const createV3Router = () => {
|
|
|
367
393
|
return;
|
|
368
394
|
}
|
|
369
395
|
|
|
396
|
+
const ifMatchHeader = req.headers['if-match'];
|
|
397
|
+
const ifMatch = Array.isArray(ifMatchHeader) ? ifMatchHeader[0] : ifMatchHeader;
|
|
398
|
+
if (ifMatch && ifMatch !== '*') {
|
|
399
|
+
const cleanIfMatch = ifMatch.replace(/^"|"$/g, '');
|
|
400
|
+
const cleanEtag = existingFile.etag.replace(/^"|"$/g, '');
|
|
401
|
+
if (cleanIfMatch !== cleanEtag) {
|
|
402
|
+
res.status(412).json({ error: { code: 412, message: "Precondition Failed" } });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
370
407
|
const uploadType = req.query.uploadType as string;
|
|
371
408
|
|
|
372
409
|
if (uploadType === 'media') {
|
|
@@ -379,7 +416,7 @@ export const createV3Router = () => {
|
|
|
379
416
|
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content),
|
|
380
417
|
modifiedTime: new Date().toISOString()
|
|
381
418
|
});
|
|
382
|
-
res.status(200).json(updatedFile!);
|
|
419
|
+
res.status(200).json(toV3File(updatedFile!));
|
|
383
420
|
return;
|
|
384
421
|
}
|
|
385
422
|
|
|
@@ -453,7 +490,7 @@ export const createV3Router = () => {
|
|
|
453
490
|
modifiedTime: new Date().toISOString()
|
|
454
491
|
});
|
|
455
492
|
|
|
456
|
-
res.status(200).json(updatedFile);
|
|
493
|
+
res.status(200).json(toV3File(updatedFile!));
|
|
457
494
|
return;
|
|
458
495
|
}
|
|
459
496
|
|
|
@@ -472,7 +509,7 @@ export const createV3Router = () => {
|
|
|
472
509
|
parents: body.parents || []
|
|
473
510
|
});
|
|
474
511
|
|
|
475
|
-
res.status(200).json(newFile);
|
|
512
|
+
res.status(200).json(toV3File(newFile));
|
|
476
513
|
});
|
|
477
514
|
|
|
478
515
|
// Files: Get
|
|
@@ -491,7 +528,19 @@ export const createV3Router = () => {
|
|
|
491
528
|
|
|
492
529
|
const fields = req.query.fields as string;
|
|
493
530
|
if (fields && (fields.includes('etag') || fields.includes('kind,etag'))) {
|
|
494
|
-
res.status(400).json({
|
|
531
|
+
res.status(400).json({
|
|
532
|
+
error: {
|
|
533
|
+
code: 400,
|
|
534
|
+
message: "Invalid field selection etag",
|
|
535
|
+
errors: [{
|
|
536
|
+
message: "Invalid field selection etag",
|
|
537
|
+
domain: "global",
|
|
538
|
+
reason: "invalidParameter",
|
|
539
|
+
location: "fields",
|
|
540
|
+
locationType: "parameter"
|
|
541
|
+
}]
|
|
542
|
+
}
|
|
543
|
+
});
|
|
495
544
|
return;
|
|
496
545
|
}
|
|
497
546
|
|
|
@@ -513,7 +562,12 @@ export const createV3Router = () => {
|
|
|
513
562
|
return;
|
|
514
563
|
}
|
|
515
564
|
|
|
516
|
-
|
|
565
|
+
const v3File = toV3File(file);
|
|
566
|
+
if (fields) {
|
|
567
|
+
res.json(applyFields(v3File, fields));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
res.json(v3File);
|
|
517
571
|
});
|
|
518
572
|
|
|
519
573
|
// Files: Update
|
|
@@ -562,7 +616,7 @@ export const createV3Router = () => {
|
|
|
562
616
|
}
|
|
563
617
|
}
|
|
564
618
|
|
|
565
|
-
res.json(updatedFile);
|
|
619
|
+
res.json(toV3File(updatedFile));
|
|
566
620
|
});
|
|
567
621
|
|
|
568
622
|
// Files: Delete
|
package/src/store.ts
CHANGED
|
@@ -95,7 +95,7 @@ export class DriveStore {
|
|
|
95
95
|
...file,
|
|
96
96
|
id,
|
|
97
97
|
version: 1, // Initialize version
|
|
98
|
-
etag: "1", // Initialize etag
|
|
98
|
+
etag: '"1"', // Initialize etag
|
|
99
99
|
// Ensure calculated stats override provided ones
|
|
100
100
|
size: stats.size,
|
|
101
101
|
md5Checksum: stats.md5Checksum
|
|
@@ -123,7 +123,7 @@ export class DriveStore {
|
|
|
123
123
|
...updates,
|
|
124
124
|
...statsUpdates,
|
|
125
125
|
version: newVersion,
|
|
126
|
-
etag:
|
|
126
|
+
etag: `"${newVersion}"`,
|
|
127
127
|
modifiedTime: updates.modifiedTime || new Date().toISOString()
|
|
128
128
|
};
|
|
129
129
|
this.files.set(id, updatedFile);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, beforeAll, vi, afterEach } from 'vitest';
|
|
2
|
+
import { it } from './config';;
|
|
2
3
|
import { getTestConfig, TestConfig } from './config';
|
|
3
4
|
import { DriveFile, DriveChange } from '../src/store';
|
|
4
5
|
|
|
@@ -29,10 +30,6 @@ describe('Advanced Drive Features (Part 1)', () => {
|
|
|
29
30
|
return { status: res.status, body: text, headers: res.headers };
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
-
if (!config.testFolderId) {
|
|
34
|
-
// ensure folder
|
|
35
|
-
}
|
|
36
33
|
});
|
|
37
34
|
|
|
38
35
|
// Rate Limit Mitigation for Real API
|
|
@@ -40,7 +37,7 @@ describe('Advanced Drive Features (Part 1)', () => {
|
|
|
40
37
|
await new Promise(r => setTimeout(r, 1000));
|
|
41
38
|
});
|
|
42
39
|
|
|
43
|
-
it('should support changes feed
|
|
40
|
+
it('should support changes feed: file creation change', async () => {
|
|
44
41
|
// 1. Get Start Page Token
|
|
45
42
|
const tokenRes = await req('GET', '/drive/v3/changes/startPageToken?supportsAllDrives=true');
|
|
46
43
|
expect(tokenRes.status).toBe(200);
|
|
@@ -48,7 +45,7 @@ describe('Advanced Drive Features (Part 1)', () => {
|
|
|
48
45
|
expect(startToken).toBeDefined();
|
|
49
46
|
|
|
50
47
|
// 2. Make a change (Create file)
|
|
51
|
-
const fileName = `ChangeTest-${Date.now()}`;
|
|
48
|
+
const fileName = `ChangeTest-Create-${Date.now()}`;
|
|
52
49
|
const createRes = await req('POST', '/drive/v3/files', {
|
|
53
50
|
name: fileName,
|
|
54
51
|
parents: [config.testFolderId]
|
|
@@ -56,23 +53,30 @@ describe('Advanced Drive Features (Part 1)', () => {
|
|
|
56
53
|
expect(createRes.status).toBe(200);
|
|
57
54
|
const fileId = (createRes.body as DriveFile).id;
|
|
58
55
|
|
|
59
|
-
// 3. List Changes
|
|
60
|
-
const changesRes = await req('GET', `/drive/v3/changes?pageToken=${startToken}&supportsAllDrives=true&fields=changes(fileId,removed,file(name))`);
|
|
61
|
-
expect(changesRes.status).toBe(200);
|
|
62
|
-
|
|
56
|
+
// 3. List Changes (poll for creation)
|
|
63
57
|
let found: DriveChange | undefined;
|
|
64
58
|
const maxRetries = 20;
|
|
65
|
-
const retryDelay =
|
|
59
|
+
const retryDelay = 300;
|
|
66
60
|
|
|
67
61
|
for (let i = 0; i < maxRetries; i++) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
let currentToken: string | undefined = startToken;
|
|
63
|
+
let foundInPage = false;
|
|
64
|
+
while (currentToken) {
|
|
65
|
+
const changesRes = await req('GET', `/drive/v3/changes?pageToken=${currentToken}&supportsAllDrives=true&includeItemsFromAllDrives=true&fields=changes(fileId,removed,file(name)),nextPageToken`);
|
|
66
|
+
expect(changesRes.status).toBe(200);
|
|
67
|
+
const body = changesRes.body as { changes: DriveChange[]; nextPageToken?: string };
|
|
68
|
+
const changes = body.changes || [];
|
|
69
|
+
found = changes.find((c: DriveChange) => c.fileId === fileId);
|
|
70
|
+
|
|
71
|
+
if (found) {
|
|
72
|
+
foundInPage = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
currentToken = body.nextPageToken;
|
|
76
|
+
}
|
|
72
77
|
|
|
73
|
-
if (
|
|
78
|
+
if (foundInPage) break;
|
|
74
79
|
|
|
75
|
-
// Wait before retry
|
|
76
80
|
if (i < maxRetries - 1) {
|
|
77
81
|
await new Promise(r => setTimeout(r, retryDelay));
|
|
78
82
|
}
|
|
@@ -84,18 +88,61 @@ describe('Advanced Drive Features (Part 1)', () => {
|
|
|
84
88
|
expect(found.file?.name).toBe(fileName);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
//
|
|
91
|
+
// Clean up without waiting
|
|
92
|
+
await req('DELETE', `/drive/v3/files/${fileId}`);
|
|
93
|
+
}, 10000);
|
|
94
|
+
|
|
95
|
+
it('should support changes feed: file deletion change', async () => {
|
|
96
|
+
// 1. Get Start Page Token
|
|
97
|
+
const tokenRes = await req('GET', '/drive/v3/changes/startPageToken?supportsAllDrives=true');
|
|
98
|
+
expect(tokenRes.status).toBe(200);
|
|
99
|
+
const startToken = (tokenRes.body as { startPageToken: string }).startPageToken;
|
|
100
|
+
expect(startToken).toBeDefined();
|
|
101
|
+
|
|
102
|
+
// 2. Create file
|
|
103
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
104
|
+
name: `ChangeTest-DeletePrep-${Date.now()}`,
|
|
105
|
+
parents: [config.testFolderId]
|
|
106
|
+
});
|
|
107
|
+
expect(createRes.status).toBe(200);
|
|
108
|
+
const fileId = (createRes.body as DriveFile).id;
|
|
109
|
+
|
|
110
|
+
// 3. Delete the file
|
|
88
111
|
await req('DELETE', `/drive/v3/files/${fileId}`);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
|
|
113
|
+
// 4. Poll changes feed for deletion
|
|
114
|
+
let deletion: DriveChange | undefined;
|
|
115
|
+
const maxRetries = 20;
|
|
116
|
+
const retryDelay = 300;
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
119
|
+
let currentToken: string | undefined = startToken;
|
|
120
|
+
let foundInPage = false;
|
|
121
|
+
while (currentToken) {
|
|
122
|
+
const changesRes2 = await req('GET', `/drive/v3/changes?pageToken=${currentToken}&supportsAllDrives=true&includeItemsFromAllDrives=true&fields=changes(fileId,removed),nextPageToken`);
|
|
123
|
+
expect(changesRes2.status).toBe(200);
|
|
124
|
+
const body = changesRes2.body as { changes: DriveChange[]; nextPageToken?: string };
|
|
125
|
+
const changes2 = body.changes || [];
|
|
126
|
+
deletion = changes2.find((c: DriveChange) => c.fileId === fileId && c.removed === true);
|
|
127
|
+
|
|
128
|
+
if (deletion) {
|
|
129
|
+
foundInPage = true;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
currentToken = body.nextPageToken;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (foundInPage) break;
|
|
136
|
+
|
|
137
|
+
if (i < maxRetries - 1) {
|
|
138
|
+
await new Promise(r => setTimeout(r, retryDelay));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
expect(deletion).toBeDefined();
|
|
143
|
+
if (deletion) {
|
|
144
|
+
expect(deletion.removed).toBe(true);
|
|
145
|
+
}
|
|
99
146
|
}, 10000);
|
|
100
147
|
|
|
101
148
|
it('should support advanced query operators (contains, in parents)', async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, beforeAll, vi, afterEach } from 'vitest';
|
|
2
|
+
import { it } from './config';;
|
|
2
3
|
import { getTestConfig, TestConfig } from './config';
|
|
3
4
|
import { DriveFile } from '../src/store';
|
|
4
5
|
|
package/test/basics.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { it } from './config';;
|
|
2
3
|
import { getTestConfig, TestConfig } from './config';
|
|
3
4
|
import { Server } from 'http';
|
|
4
5
|
|
|
@@ -75,12 +76,11 @@ describe('Google Drive Mock API', () => {
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
describe('Files API', () => {
|
|
78
|
-
|
|
79
|
+
const fileName = 'Test File ' + Math.random().toString(36).substring(7);
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
it('POST /drive/v3/files - should create a file (Happy Path)', async () => {
|
|
81
|
+
it('should support full file lifecycle: create, get, update, delete', async () => {
|
|
82
82
|
const newFile = {
|
|
83
|
-
name:
|
|
83
|
+
name: fileName,
|
|
84
84
|
mimeType: 'text/plain',
|
|
85
85
|
parents: [config.testFolderId]
|
|
86
86
|
};
|
|
@@ -89,8 +89,27 @@ describe('Google Drive Mock API', () => {
|
|
|
89
89
|
expect(response.status).toBe(200);
|
|
90
90
|
expect(response.body.name).toBe(newFile.name);
|
|
91
91
|
expect(response.body.id).toBeDefined();
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
const createdFileId = response.body.id;
|
|
93
|
+
|
|
94
|
+
// 2. Get File
|
|
95
|
+
const responseGet = await req('GET', `/drive/v3/files/${createdFileId}`);
|
|
96
|
+
expect(responseGet.status).toBe(200);
|
|
97
|
+
expect(responseGet.body.id).toBe(createdFileId);
|
|
98
|
+
expect(responseGet.body.name).toBe(fileName);
|
|
99
|
+
|
|
100
|
+
// 3. Update File
|
|
101
|
+
const updatedName = 'Updated Name ' + Math.random().toString(36).substring(7);
|
|
102
|
+
const responsePatch = await req('PATCH', `/drive/v3/files/${createdFileId}`, { name: updatedName });
|
|
103
|
+
expect(responsePatch.status).toBe(200);
|
|
104
|
+
expect(responsePatch.body.name).toBe(updatedName);
|
|
105
|
+
|
|
106
|
+
// 4. Delete File
|
|
107
|
+
const responseDelete = await req('DELETE', `/drive/v3/files/${createdFileId}`);
|
|
108
|
+
expect(responseDelete.status).toBe(204);
|
|
109
|
+
|
|
110
|
+
// 5. Verify Deletion
|
|
111
|
+
const responseCheck = await req('GET', `/drive/v3/files/${createdFileId}`);
|
|
112
|
+
expect(responseCheck.status).toBe(404);
|
|
94
113
|
});
|
|
95
114
|
|
|
96
115
|
it('POST /drive/v3/files - should allow file creation without name (defaults to Untitled?)', async () => {
|
|
@@ -104,69 +123,26 @@ describe('Google Drive Mock API', () => {
|
|
|
104
123
|
// Optionally check name if we want to be strict about "Untitled".
|
|
105
124
|
// For now, status 200 is the key parity requirement.
|
|
106
125
|
});
|
|
107
|
-
|
|
108
|
-
// 2. Get File
|
|
109
|
-
it('GET /drive/v3/files/:id - should get file', async () => {
|
|
110
|
-
// Need to verify createdFileId exists (if previous test failed, this might fail or throw)
|
|
111
|
-
if (!createdFileId) return; // Skip
|
|
112
|
-
|
|
113
|
-
const response = await req('GET', `/drive/v3/files/${createdFileId}`);
|
|
114
|
-
|
|
115
|
-
expect(response.status).toBe(200);
|
|
116
|
-
expect(response.body.id).toBe(createdFileId);
|
|
117
|
-
expect(response.body.name).toBe('Test File');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// 3. Update File
|
|
121
|
-
it('PATCH /drive/v3/files/:id - should update file', async () => {
|
|
122
|
-
if (!createdFileId) return;
|
|
123
|
-
|
|
124
|
-
const response = await req('PATCH', `/drive/v3/files/${createdFileId}`, { name: 'Updated Name' });
|
|
125
|
-
|
|
126
|
-
expect(response.status).toBe(200);
|
|
127
|
-
expect(response.body.name).toBe('Updated Name');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// 4. Delete File
|
|
131
|
-
it('DELETE /drive/v3/files/:id - should delete file', async () => {
|
|
132
|
-
if (!createdFileId) return;
|
|
133
|
-
const response = await req('DELETE', `/drive/v3/files/${createdFileId}`);
|
|
134
|
-
expect(response.status).toBe(204);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// 5. Verify Deletion
|
|
138
|
-
it('GET /drive/v3/files/:id - should return 404 after delete', async () => {
|
|
139
|
-
if (!createdFileId) return;
|
|
140
|
-
const response = await req('GET', `/drive/v3/files/${createdFileId}`);
|
|
141
|
-
expect(response.status).toBe(404);
|
|
142
|
-
});
|
|
143
126
|
});
|
|
144
127
|
|
|
145
128
|
describe('Folders API', () => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
it('should create a new folder', async () => {
|
|
129
|
+
it('should support full folder lifecycle: create, delete, check 404', async () => {
|
|
130
|
+
const folderName = 'Test Folder ' + Math.random().toString(36).substring(7);
|
|
149
131
|
const folder = {
|
|
150
|
-
name:
|
|
132
|
+
name: folderName,
|
|
151
133
|
mimeType: 'application/vnd.google-apps.folder',
|
|
152
134
|
parents: [config.testFolderId]
|
|
153
135
|
};
|
|
154
136
|
const res = await req('POST', '/drive/v3/files', folder);
|
|
155
137
|
expect(res.status).toBe(200);
|
|
156
138
|
expect(res.body.mimeType).toBe('application/vnd.google-apps.folder');
|
|
157
|
-
folderId = res.body.id;
|
|
158
|
-
});
|
|
139
|
+
const folderId = res.body.id;
|
|
159
140
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const res = await req('DELETE', `/drive/v3/files/${folderId}`);
|
|
163
|
-
expect(res.status).toBe(204);
|
|
164
|
-
});
|
|
141
|
+
const resDelete = await req('DELETE', `/drive/v3/files/${folderId}`);
|
|
142
|
+
expect(resDelete.status).toBe(204);
|
|
165
143
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const res = await req('GET', `/drive/v3/files/${folderId}`);
|
|
169
|
-
expect(res.status).toBe(404);
|
|
144
|
+
const resCheck = await req('GET', `/drive/v3/files/${folderId}`);
|
|
145
|
+
expect(resCheck.status).toBe(404);
|
|
170
146
|
});
|
|
171
147
|
});
|
|
172
148
|
|
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { it } from './config';
|
|
3
3
|
import { getTestConfig, TestConfig } from './config';
|
|
4
4
|
|
|
5
5
|
describe('Batch and Complex Query Operations', () => {
|
|
6
6
|
let config: TestConfig;
|
|
7
|
-
let createdFileIds: string[] = [];
|
|
8
7
|
|
|
9
8
|
beforeAll(async () => {
|
|
10
9
|
config = await getTestConfig();
|
|
11
10
|
});
|
|
12
11
|
|
|
13
12
|
afterAll(async () => {
|
|
14
|
-
// Cleanup if needed
|
|
15
13
|
if (config) config.stop();
|
|
16
14
|
});
|
|
17
15
|
|
|
18
|
-
it('should perform bulk insert using batch
|
|
16
|
+
it('should perform bulk insert, find, and update operations using batch and complex queries', async () => {
|
|
19
17
|
const docs = [
|
|
20
18
|
{ id: 'bulk1', content: '{"foo":1}' },
|
|
21
19
|
{ id: 'bulk2', content: '{"bar":2}' }
|
|
22
20
|
];
|
|
23
21
|
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
22
|
const targetFolderId = config.testFolderId;
|
|
27
23
|
|
|
28
24
|
const parts = docs.map((doc, i) => {
|
|
@@ -44,7 +40,6 @@ describe('Batch and Complex Query Operations', () => {
|
|
|
44
40
|
});
|
|
45
41
|
|
|
46
42
|
const batchBody = parts.join("") + `--${boundary}--`;
|
|
47
|
-
|
|
48
43
|
const url = config.baseUrl + "/batch/drive/v3";
|
|
49
44
|
console.log('Sending batch insert request to:', url);
|
|
50
45
|
|
|
@@ -64,28 +59,18 @@ describe('Batch and Complex Query Operations', () => {
|
|
|
64
59
|
|
|
65
60
|
const text = await res.text();
|
|
66
61
|
console.log('Batch Insert Response:', text);
|
|
67
|
-
|
|
68
|
-
// Verify operations succeeded
|
|
69
62
|
expect(text).toContain('HTTP/1.1 200 OK');
|
|
70
63
|
|
|
71
|
-
// Extract IDs for later use
|
|
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
|
|
64
|
+
// Extract IDs for later use
|
|
75
65
|
const listRes = await fetch(config.baseUrl + `/drive/v3/files?q='${targetFolderId}'+in+parents`, {
|
|
76
66
|
headers: { Authorization: `Bearer ${config.token}` }
|
|
77
67
|
});
|
|
78
68
|
const listData = await listRes.json();
|
|
79
69
|
const files = listData.files.filter((f: { name: string; id: string }) => f.name === 'bulk1.json' || f.name === 'bulk2.json');
|
|
80
70
|
expect(files.length).toBe(2);
|
|
81
|
-
createdFileIds = files.map((f: { id: string }) => f.id);
|
|
82
|
-
});
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
// Ensure the files exist from previous test
|
|
86
|
-
expect(createdFileIds.length).toBe(2);
|
|
72
|
+
// 2. Perform bulk find using complex query
|
|
87
73
|
const docIds = ['bulk1', 'bulk2'];
|
|
88
|
-
|
|
89
74
|
const fileNames = docIds.map(id => id + '.json');
|
|
90
75
|
let q = fileNames
|
|
91
76
|
.map(name => `name = '${name.replace("'", "\\'")}'`)
|
|
@@ -101,58 +86,42 @@ describe('Batch and Complex Query Operations', () => {
|
|
|
101
86
|
includeItemsFromAllDrives: "true",
|
|
102
87
|
supportsAllDrives: "true",
|
|
103
88
|
});
|
|
104
|
-
const
|
|
105
|
-
const
|
|
89
|
+
const findUrl = config.baseUrl + '/drive/v3/files?' + params.toString();
|
|
90
|
+
const findRes = await fetch(findUrl, {
|
|
106
91
|
method: "GET",
|
|
107
92
|
headers: {
|
|
108
93
|
Authorization: `Bearer ${config.token}`,
|
|
109
94
|
},
|
|
110
95
|
});
|
|
111
96
|
|
|
112
|
-
expect(
|
|
113
|
-
const
|
|
97
|
+
expect(findRes.status).toBe(200);
|
|
98
|
+
const findData = await findRes.json();
|
|
114
99
|
|
|
115
|
-
|
|
116
|
-
|
|
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);
|
|
100
|
+
expect(findData.files).toBeDefined();
|
|
101
|
+
const foundNames = findData.files.map((f: { name: string }) => f.name);
|
|
120
102
|
expect(foundNames).toContain('bulk1.json');
|
|
121
103
|
expect(foundNames).toContain('bulk2.json');
|
|
122
|
-
expect(
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should perform bulk update using batch API', async () => {
|
|
126
|
-
expect(createdFileIds.length).toBe(2);
|
|
104
|
+
expect(findData.files.length).toBeGreaterThanOrEqual(2);
|
|
127
105
|
|
|
106
|
+
// 3. Perform bulk update using batch API
|
|
128
107
|
interface DocUpdate {
|
|
129
108
|
id: string;
|
|
130
109
|
newName: string;
|
|
131
110
|
}
|
|
132
|
-
const
|
|
111
|
+
const docUpdates: DocUpdate[] = [
|
|
133
112
|
{ id: 'bulk1', newName: 'bulk1_updated' },
|
|
134
113
|
{ id: 'bulk2', newName: 'bulk2_updated' }
|
|
135
114
|
];
|
|
136
115
|
|
|
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
116
|
const fileIdByDocId: Record<string, string> = {};
|
|
148
|
-
for (const f of
|
|
117
|
+
for (const f of findData.files) {
|
|
149
118
|
if (f.name === 'bulk1.json') fileIdByDocId['bulk1'] = f.id;
|
|
150
119
|
if (f.name === 'bulk2.json') fileIdByDocId['bulk2'] = f.id;
|
|
151
120
|
}
|
|
152
121
|
|
|
153
|
-
const
|
|
122
|
+
const updateBoundary = "batch_" + Math.random().toString(16).slice(2);
|
|
154
123
|
|
|
155
|
-
const
|
|
124
|
+
const updateParts = docUpdates.map((doc, i) => {
|
|
156
125
|
const id = doc.id;
|
|
157
126
|
const fileId = fileIdByDocId[id];
|
|
158
127
|
if (!fileId) throw new Error(`File ID not found for ${id}`);
|
|
@@ -160,11 +129,10 @@ describe('Batch and Complex Query Operations', () => {
|
|
|
160
129
|
const body = JSON.stringify({
|
|
161
130
|
name: doc.newName + '.json',
|
|
162
131
|
mimeType: "application/json",
|
|
163
|
-
// parents: [config.testFolderId], // Optional in update usually
|
|
164
132
|
});
|
|
165
133
|
|
|
166
134
|
return (
|
|
167
|
-
`--${
|
|
135
|
+
`--${updateBoundary}\r\n` +
|
|
168
136
|
`Content-Type: application/http\r\n` +
|
|
169
137
|
`Content-ID: <item-${i}>\r\n\r\n` +
|
|
170
138
|
`PATCH /drive/v3/files/${encodeURIComponent(fileId)}?supportsAllDrives=true&fields=id,name,mimeType,parents HTTP/1.1\r\n` +
|
|
@@ -173,25 +141,23 @@ describe('Batch and Complex Query Operations', () => {
|
|
|
173
141
|
);
|
|
174
142
|
});
|
|
175
143
|
|
|
176
|
-
const
|
|
144
|
+
const updateBatchBody = updateParts.join("") + `--${updateBoundary}--`;
|
|
145
|
+
const updateUrl = config.baseUrl + "/batch/drive/v3";
|
|
146
|
+
console.log('Sending batch update request to:', updateUrl);
|
|
177
147
|
|
|
178
|
-
const
|
|
179
|
-
console.log('Sending batch update request to:', url);
|
|
180
|
-
|
|
181
|
-
const res = await fetch(url, {
|
|
148
|
+
const updateRes = await fetch(updateUrl, {
|
|
182
149
|
method: "POST",
|
|
183
150
|
headers: {
|
|
184
151
|
Authorization: `Bearer ${config.token}`,
|
|
185
|
-
"Content-Type": `multipart/mixed; boundary=${
|
|
152
|
+
"Content-Type": `multipart/mixed; boundary=${updateBoundary}`,
|
|
186
153
|
},
|
|
187
|
-
body:
|
|
154
|
+
body: updateBatchBody,
|
|
188
155
|
});
|
|
189
156
|
|
|
190
|
-
expect(
|
|
191
|
-
const
|
|
192
|
-
console.log('Batch Update Response:',
|
|
193
|
-
|
|
194
|
-
expect(text).toContain('HTTP/1.1 200 OK');
|
|
157
|
+
expect(updateRes.status).toBe(200);
|
|
158
|
+
const updateText = await updateRes.text();
|
|
159
|
+
console.log('Batch Update Response:', updateText);
|
|
160
|
+
expect(updateText).toContain('HTTP/1.1 200 OK');
|
|
195
161
|
|
|
196
162
|
// Verify updates
|
|
197
163
|
const verifyRes = await fetch(config.baseUrl + `/drive/v3/files?q='${config.testFolderId}'+in+parents`, {
|