google-drive-mock 1.1.6 → 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 +30 -9
- 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 +35 -10
- 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 +153 -68
- 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 +56 -20
- 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) {
|
|
@@ -260,11 +260,20 @@ export const createV3Router = () => {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
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
|
+
});
|
|
263
272
|
res.json({
|
|
264
273
|
kind: "drive#changeList",
|
|
265
274
|
newStartPageToken: result.newStartPageToken,
|
|
266
275
|
nextPageToken: result.nextPageToken,
|
|
267
|
-
changes:
|
|
276
|
+
changes: mappedChanges
|
|
268
277
|
});
|
|
269
278
|
});
|
|
270
279
|
|
|
@@ -292,7 +301,7 @@ export const createV3Router = () => {
|
|
|
292
301
|
parents: [],
|
|
293
302
|
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content)
|
|
294
303
|
});
|
|
295
|
-
res.status(200).json(newFile);
|
|
304
|
+
res.status(200).json(toV3File(newFile));
|
|
296
305
|
return;
|
|
297
306
|
}
|
|
298
307
|
|
|
@@ -367,7 +376,7 @@ export const createV3Router = () => {
|
|
|
367
376
|
content: content
|
|
368
377
|
});
|
|
369
378
|
|
|
370
|
-
res.status(200).json(newFile);
|
|
379
|
+
res.status(200).json(toV3File(newFile));
|
|
371
380
|
});
|
|
372
381
|
|
|
373
382
|
// Upload Files: Update (PATCH)
|
|
@@ -384,6 +393,17 @@ export const createV3Router = () => {
|
|
|
384
393
|
return;
|
|
385
394
|
}
|
|
386
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
|
+
|
|
387
407
|
const uploadType = req.query.uploadType as string;
|
|
388
408
|
|
|
389
409
|
if (uploadType === 'media') {
|
|
@@ -396,7 +416,7 @@ export const createV3Router = () => {
|
|
|
396
416
|
content: typeof content === 'string' || Buffer.isBuffer(content) ? content : JSON.stringify(content),
|
|
397
417
|
modifiedTime: new Date().toISOString()
|
|
398
418
|
});
|
|
399
|
-
res.status(200).json(updatedFile!);
|
|
419
|
+
res.status(200).json(toV3File(updatedFile!));
|
|
400
420
|
return;
|
|
401
421
|
}
|
|
402
422
|
|
|
@@ -470,7 +490,7 @@ export const createV3Router = () => {
|
|
|
470
490
|
modifiedTime: new Date().toISOString()
|
|
471
491
|
});
|
|
472
492
|
|
|
473
|
-
res.status(200).json(updatedFile);
|
|
493
|
+
res.status(200).json(toV3File(updatedFile!));
|
|
474
494
|
return;
|
|
475
495
|
}
|
|
476
496
|
|
|
@@ -489,7 +509,7 @@ export const createV3Router = () => {
|
|
|
489
509
|
parents: body.parents || []
|
|
490
510
|
});
|
|
491
511
|
|
|
492
|
-
res.status(200).json(newFile);
|
|
512
|
+
res.status(200).json(toV3File(newFile));
|
|
493
513
|
});
|
|
494
514
|
|
|
495
515
|
// Files: Get
|
|
@@ -542,7 +562,12 @@ export const createV3Router = () => {
|
|
|
542
562
|
return;
|
|
543
563
|
}
|
|
544
564
|
|
|
545
|
-
|
|
565
|
+
const v3File = toV3File(file);
|
|
566
|
+
if (fields) {
|
|
567
|
+
res.json(applyFields(v3File, fields));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
res.json(v3File);
|
|
546
571
|
});
|
|
547
572
|
|
|
548
573
|
// Files: Update
|
|
@@ -591,7 +616,7 @@ export const createV3Router = () => {
|
|
|
591
616
|
}
|
|
592
617
|
}
|
|
593
618
|
|
|
594
|
-
res.json(updatedFile);
|
|
619
|
+
res.json(toV3File(updatedFile));
|
|
595
620
|
});
|
|
596
621
|
|
|
597
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&includeItemsFromAllDrives=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`, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
import { describe,
|
|
2
|
+
import { describe, expect, beforeAll, afterAll } from 'vitest';
|
|
3
|
+
import { it } from './config';;
|
|
3
4
|
import { getTestConfig, TestConfig } from './config';
|
|
4
5
|
|
|
5
6
|
describe('Batch Insert and Download Test', () => {
|