google-drive-mock 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/dist/index.js +38 -5
- package/package.json +1 -1
- package/src/index.ts +39 -4
- package/test/etag.test.ts +173 -0
- package/test/routines.test.ts +2 -18
- package/test/upload.test.ts +82 -0
package/AGENTS.md
CHANGED
|
@@ -7,3 +7,8 @@ After making changes to the codebase or to the tests, always run the following c
|
|
|
7
7
|
3. `npm test`
|
|
8
8
|
4. `npm test:browser`
|
|
9
9
|
5. `npm test:real`
|
|
10
|
+
|
|
11
|
+
## Remember
|
|
12
|
+
|
|
13
|
+
Whenever the `npm test:real` fails, fix the tests to match the "real" backend.
|
|
14
|
+
Ensure that the behavior of the mock is exactly equal to the "real" backend.
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const batch_1 = require("./batch");
|
|
|
20
20
|
const createApp = (config = {}) => {
|
|
21
21
|
const app = (0, express_1.default)();
|
|
22
22
|
app.use((0, cors_1.default)());
|
|
23
|
+
app.set('etag', false); // Disable default ETag generation to match Real API behavior
|
|
23
24
|
app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
25
|
if (config.serverLagBefore && config.serverLagBefore > 0) {
|
|
25
26
|
yield new Promise(resolve => setTimeout(resolve, config.serverLagBefore));
|
|
@@ -277,7 +278,20 @@ const createApp = (config = {}) => {
|
|
|
277
278
|
}
|
|
278
279
|
// Create File
|
|
279
280
|
// Ensure name uniqueness check if needed (reusing logic from normal create)
|
|
280
|
-
const existing = store_1.driveStore.listFiles().find(f =>
|
|
281
|
+
const existing = store_1.driveStore.listFiles().find(f => {
|
|
282
|
+
if (f.name !== metadata.name)
|
|
283
|
+
return false;
|
|
284
|
+
// Filter trashed?
|
|
285
|
+
if (f.trashed)
|
|
286
|
+
return false;
|
|
287
|
+
const newParents = metadata.parents || [];
|
|
288
|
+
const existingParents = f.parents || [];
|
|
289
|
+
// If both new and existing have NO parents, they are both in root -> Conflict
|
|
290
|
+
if (newParents.length === 0 && existingParents.length === 0)
|
|
291
|
+
return true;
|
|
292
|
+
// Check intersection of parents
|
|
293
|
+
return newParents.some((p) => existingParents.includes(p));
|
|
294
|
+
});
|
|
281
295
|
if (existing) {
|
|
282
296
|
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
283
297
|
return;
|
|
@@ -293,7 +307,17 @@ const createApp = (config = {}) => {
|
|
|
293
307
|
return;
|
|
294
308
|
}
|
|
295
309
|
// Enforce Unique Name Constraint (Mock Behavior customization)
|
|
296
|
-
const existing = store_1.driveStore.listFiles().find(f =>
|
|
310
|
+
const existing = store_1.driveStore.listFiles().find(f => {
|
|
311
|
+
if (f.name !== body.name)
|
|
312
|
+
return false;
|
|
313
|
+
if (f.trashed)
|
|
314
|
+
return false;
|
|
315
|
+
const newParents = body.parents || [];
|
|
316
|
+
const existingParents = f.parents || [];
|
|
317
|
+
if (newParents.length === 0 && existingParents.length === 0)
|
|
318
|
+
return true;
|
|
319
|
+
return newParents.some((p) => existingParents.includes(p));
|
|
320
|
+
});
|
|
297
321
|
if (existing) {
|
|
298
322
|
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
299
323
|
return;
|
|
@@ -313,12 +337,16 @@ const createApp = (config = {}) => {
|
|
|
313
337
|
res.status(404).json({ error: { code: 404, message: "File not found" } });
|
|
314
338
|
return;
|
|
315
339
|
}
|
|
316
|
-
|
|
317
|
-
res.setHeader('ETag', etag);
|
|
340
|
+
// Mock does not return ETag header because Real API (v3) does not return it by default/in this context.
|
|
341
|
+
// res.setHeader('ETag', etag);
|
|
342
|
+
// Real API also ignores If-None-Match if ETag is not supported?
|
|
343
|
+
// match behavior: do nothing.
|
|
344
|
+
/*
|
|
318
345
|
if (req.headers['if-none-match'] === etag) {
|
|
319
346
|
res.status(304).end();
|
|
320
347
|
return;
|
|
321
348
|
}
|
|
349
|
+
*/
|
|
322
350
|
res.json(file);
|
|
323
351
|
});
|
|
324
352
|
// Files: Update
|
|
@@ -337,6 +365,8 @@ const createApp = (config = {}) => {
|
|
|
337
365
|
// Note: Real Google Drive API V3 was observed to allow overwrites (status 200)
|
|
338
366
|
// on PATCH even with mismatching If-Match headers (likely due to ETag generation nuances).
|
|
339
367
|
// Relaxing Mock to match Real API behavior (Last Write Wins).
|
|
368
|
+
// Real API V3 observed behavior: Ignores If-Match (Last Write Wins).
|
|
369
|
+
// Mock matches this.
|
|
340
370
|
/*
|
|
341
371
|
const existingFile = driveStore.getFile(fileId);
|
|
342
372
|
if (existingFile) {
|
|
@@ -362,7 +392,9 @@ const createApp = (config = {}) => {
|
|
|
362
392
|
return;
|
|
363
393
|
}
|
|
364
394
|
// Check for Precondition (If-Match)
|
|
365
|
-
|
|
395
|
+
// Real API behavior: Ignores If-Match (returns 204 even on mismatch)
|
|
396
|
+
/*
|
|
397
|
+
const existingFile = driveStore.getFile(fileId);
|
|
366
398
|
if (existingFile) {
|
|
367
399
|
const ifMatch = req.headers['if-match'];
|
|
368
400
|
if (ifMatch && ifMatch !== '*' && ifMatch !== `"${existingFile.version}"`) {
|
|
@@ -370,6 +402,7 @@ const createApp = (config = {}) => {
|
|
|
370
402
|
return;
|
|
371
403
|
}
|
|
372
404
|
}
|
|
405
|
+
*/
|
|
373
406
|
const deleted = store_1.driveStore.deleteFile(fileId);
|
|
374
407
|
if (!deleted) {
|
|
375
408
|
// According to Google API, delete might return 404 if not found, or 204 if successful (or 200).
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface AppConfig {
|
|
|
11
11
|
const createApp = (config: AppConfig = {}) => {
|
|
12
12
|
const app = express();
|
|
13
13
|
app.use(cors());
|
|
14
|
+
app.set('etag', false); // Disable default ETag generation to match Real API behavior
|
|
14
15
|
|
|
15
16
|
app.use(async (req, res, next) => {
|
|
16
17
|
if (config.serverLagBefore && config.serverLagBefore > 0) {
|
|
@@ -300,7 +301,21 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
300
301
|
|
|
301
302
|
// Create File
|
|
302
303
|
// Ensure name uniqueness check if needed (reusing logic from normal create)
|
|
303
|
-
const existing = driveStore.listFiles().find(f =>
|
|
304
|
+
const existing = driveStore.listFiles().find(f => {
|
|
305
|
+
if (f.name !== metadata.name) return false;
|
|
306
|
+
// Filter trashed?
|
|
307
|
+
if (f.trashed) return false;
|
|
308
|
+
|
|
309
|
+
const newParents = metadata.parents || [];
|
|
310
|
+
const existingParents = f.parents || [];
|
|
311
|
+
|
|
312
|
+
// If both new and existing have NO parents, they are both in root -> Conflict
|
|
313
|
+
if (newParents.length === 0 && existingParents.length === 0) return true;
|
|
314
|
+
|
|
315
|
+
// Check intersection of parents
|
|
316
|
+
return newParents.some((p: string) => existingParents.includes(p));
|
|
317
|
+
});
|
|
318
|
+
|
|
304
319
|
if (existing) {
|
|
305
320
|
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
306
321
|
return;
|
|
@@ -323,7 +338,18 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
323
338
|
}
|
|
324
339
|
|
|
325
340
|
// Enforce Unique Name Constraint (Mock Behavior customization)
|
|
326
|
-
const existing = driveStore.listFiles().find(f =>
|
|
341
|
+
const existing = driveStore.listFiles().find(f => {
|
|
342
|
+
if (f.name !== body.name) return false;
|
|
343
|
+
if (f.trashed) return false;
|
|
344
|
+
|
|
345
|
+
const newParents = body.parents || [];
|
|
346
|
+
const existingParents = f.parents || [];
|
|
347
|
+
|
|
348
|
+
if (newParents.length === 0 && existingParents.length === 0) return true;
|
|
349
|
+
|
|
350
|
+
return newParents.some((p: string) => existingParents.includes(p));
|
|
351
|
+
});
|
|
352
|
+
|
|
327
353
|
if (existing) {
|
|
328
354
|
res.status(409).json({ error: { code: 409, message: "Conflict: File with same name already exists" } });
|
|
329
355
|
return;
|
|
@@ -353,13 +379,17 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
353
379
|
return;
|
|
354
380
|
}
|
|
355
381
|
|
|
356
|
-
|
|
357
|
-
res.setHeader('ETag', etag);
|
|
382
|
+
// Mock does not return ETag header because Real API (v3) does not return it by default/in this context.
|
|
383
|
+
// res.setHeader('ETag', etag);
|
|
358
384
|
|
|
385
|
+
// Real API also ignores If-None-Match if ETag is not supported?
|
|
386
|
+
// match behavior: do nothing.
|
|
387
|
+
/*
|
|
359
388
|
if (req.headers['if-none-match'] === etag) {
|
|
360
389
|
res.status(304).end();
|
|
361
390
|
return;
|
|
362
391
|
}
|
|
392
|
+
*/
|
|
363
393
|
|
|
364
394
|
res.json(file);
|
|
365
395
|
});
|
|
@@ -382,6 +412,8 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
382
412
|
// Note: Real Google Drive API V3 was observed to allow overwrites (status 200)
|
|
383
413
|
// on PATCH even with mismatching If-Match headers (likely due to ETag generation nuances).
|
|
384
414
|
// Relaxing Mock to match Real API behavior (Last Write Wins).
|
|
415
|
+
// Real API V3 observed behavior: Ignores If-Match (Last Write Wins).
|
|
416
|
+
// Mock matches this.
|
|
385
417
|
/*
|
|
386
418
|
const existingFile = driveStore.getFile(fileId);
|
|
387
419
|
if (existingFile) {
|
|
@@ -411,6 +443,8 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
411
443
|
return;
|
|
412
444
|
}
|
|
413
445
|
// Check for Precondition (If-Match)
|
|
446
|
+
// Real API behavior: Ignores If-Match (returns 204 even on mismatch)
|
|
447
|
+
/*
|
|
414
448
|
const existingFile = driveStore.getFile(fileId);
|
|
415
449
|
if (existingFile) {
|
|
416
450
|
const ifMatch = req.headers['if-match'];
|
|
@@ -419,6 +453,7 @@ const createApp = (config: AppConfig = {}) => {
|
|
|
419
453
|
return;
|
|
420
454
|
}
|
|
421
455
|
}
|
|
456
|
+
*/
|
|
422
457
|
|
|
423
458
|
const deleted = driveStore.deleteFile(fileId);
|
|
424
459
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { getTestConfig, TestConfig } from './config';
|
|
3
|
+
import { Server } from 'http';
|
|
4
|
+
|
|
5
|
+
// Helper to handle both Server (Node) and URL (Browser)
|
|
6
|
+
async function makeRequest(
|
|
7
|
+
target: Server | string,
|
|
8
|
+
method: string,
|
|
9
|
+
path: string,
|
|
10
|
+
headers: Record<string, string>,
|
|
11
|
+
body?: unknown
|
|
12
|
+
) {
|
|
13
|
+
if (typeof target === 'string') {
|
|
14
|
+
const url = `${target}${path}`;
|
|
15
|
+
const fetchOptions: RequestInit = {
|
|
16
|
+
method: method,
|
|
17
|
+
headers: headers
|
|
18
|
+
};
|
|
19
|
+
if (body) {
|
|
20
|
+
if (typeof body === 'string') {
|
|
21
|
+
fetchOptions.body = body;
|
|
22
|
+
} else {
|
|
23
|
+
fetchOptions.body = JSON.stringify(body);
|
|
24
|
+
if (!headers['Content-Type']) {
|
|
25
|
+
headers['Content-Type'] = 'application/json';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url, fetchOptions);
|
|
31
|
+
|
|
32
|
+
const resBody = res.headers.get('content-type')?.includes('application/json')
|
|
33
|
+
? await res.json()
|
|
34
|
+
: await res.text();
|
|
35
|
+
|
|
36
|
+
return { status: res.status, body: resBody, headers: res.headers };
|
|
37
|
+
} else {
|
|
38
|
+
const addr = target.address();
|
|
39
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
40
|
+
const baseUrl = `http://localhost:${port}`;
|
|
41
|
+
|
|
42
|
+
return makeRequest(baseUrl, method, path, headers, body);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('ETag and If-Match Support', () => {
|
|
47
|
+
let config: TestConfig;
|
|
48
|
+
|
|
49
|
+
beforeAll(async () => {
|
|
50
|
+
config = await getTestConfig();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
if (config) config.stop();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
async function req(method: string, path: string, body?: unknown, customHeaders: Record<string, string> = {}) {
|
|
58
|
+
const headers = {
|
|
59
|
+
'Authorization': `Bearer ${config.token}`,
|
|
60
|
+
...customHeaders
|
|
61
|
+
};
|
|
62
|
+
return makeRequest(config.target, method, path, headers, body);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
it('should NOT return ETag header when getting a file (Parity with Real API)', async () => {
|
|
66
|
+
// Create file
|
|
67
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
68
|
+
name: 'ETag Test File',
|
|
69
|
+
mimeType: 'text/plain',
|
|
70
|
+
parents: [config.testFolderId]
|
|
71
|
+
});
|
|
72
|
+
expect(createRes.status).toBe(200);
|
|
73
|
+
const fileId = createRes.body.id;
|
|
74
|
+
|
|
75
|
+
// Get file
|
|
76
|
+
const getRes = await req('GET', `/drive/v3/files/${fileId}?fields=*`);
|
|
77
|
+
|
|
78
|
+
expect(getRes.status).toBe(200);
|
|
79
|
+
// Real API v3 does not return 'etag' header in this context.
|
|
80
|
+
expect(getRes.headers.get('etag')).toBeFalsy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should ignore If-Match (return 200) even if valid (Parity with Real API)', async () => {
|
|
84
|
+
// Create file
|
|
85
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
86
|
+
name: 'If-Match Success File',
|
|
87
|
+
parents: [config.testFolderId]
|
|
88
|
+
});
|
|
89
|
+
const fileId = createRes.body.id;
|
|
90
|
+
|
|
91
|
+
// Even if we send a "valid" hypothetical etag, it should pass (200)
|
|
92
|
+
// because Real API allows overwrites and seems to ignore If-Match if no ETag is present/supported.
|
|
93
|
+
const updateRes = await req('PATCH', `/drive/v3/files/${fileId}`, {
|
|
94
|
+
name: 'Updated Name'
|
|
95
|
+
}, {
|
|
96
|
+
'If-Match': '"1"'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(updateRes.status).toBe(200);
|
|
100
|
+
expect(updateRes.body.name).toBe('Updated Name');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should ignore If-Match (return 200) if If-Match does not match (Parity with Real API)', async () => {
|
|
104
|
+
// Create file
|
|
105
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
106
|
+
name: 'If-Match Fail File',
|
|
107
|
+
parents: [config.testFolderId]
|
|
108
|
+
});
|
|
109
|
+
const fileId = createRes.body.id;
|
|
110
|
+
|
|
111
|
+
// Update with incorrect If-Match
|
|
112
|
+
const updateRes = await req('PATCH', `/drive/v3/files/${fileId}`, {
|
|
113
|
+
name: 'Should Update Anyway'
|
|
114
|
+
}, {
|
|
115
|
+
'If-Match': '"invalid-tag"'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Real API returns 200 (Last Write Wins)
|
|
119
|
+
expect(updateRes.status).toBe(200);
|
|
120
|
+
expect(updateRes.body.name).toBe('Should Update Anyway');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should allow PATCH if If-Match is * (Wildcard)', async () => {
|
|
124
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
125
|
+
name: 'Wildcard File',
|
|
126
|
+
parents: [config.testFolderId]
|
|
127
|
+
});
|
|
128
|
+
const fileId = createRes.body.id;
|
|
129
|
+
|
|
130
|
+
const updateRes = await req('PATCH', `/drive/v3/files/${fileId}`, {
|
|
131
|
+
name: 'Wildcard Updated'
|
|
132
|
+
}, {
|
|
133
|
+
'If-Match': '*'
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(updateRes.status).toBe(200);
|
|
137
|
+
expect(updateRes.body.name).toBe('Wildcard Updated');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should allow DELETE if If-Match matches (Parity)', async () => {
|
|
141
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
142
|
+
name: 'Delete Success File',
|
|
143
|
+
parents: [config.testFolderId]
|
|
144
|
+
});
|
|
145
|
+
const fileId = createRes.body.id;
|
|
146
|
+
|
|
147
|
+
const deleteRes = await req('DELETE', `/drive/v3/files/${fileId}`, undefined, {
|
|
148
|
+
'If-Match': '"1"'
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(deleteRes.status).toBe(204);
|
|
152
|
+
const verifyRes = await req('GET', `/drive/v3/files/${fileId}`);
|
|
153
|
+
expect(verifyRes.status).toBe(404);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should allow DELETE (204) if If-Match does not match (Parity)', async () => {
|
|
157
|
+
const createRes = await req('POST', '/drive/v3/files', {
|
|
158
|
+
name: 'Delete Fail File',
|
|
159
|
+
parents: [config.testFolderId]
|
|
160
|
+
});
|
|
161
|
+
const fileId = createRes.body.id;
|
|
162
|
+
|
|
163
|
+
const deleteRes = await req('DELETE', `/drive/v3/files/${fileId}`, undefined, {
|
|
164
|
+
'If-Match': '"wrong-tag"'
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Real API returns 204 (Delete succeeded despite If-Match header)
|
|
168
|
+
expect(deleteRes.status).toBe(204);
|
|
169
|
+
|
|
170
|
+
const verifyRes = await req('GET', `/drive/v3/files/${fileId}`);
|
|
171
|
+
expect(verifyRes.status).toBe(404);
|
|
172
|
+
});
|
|
173
|
+
});
|
package/test/routines.test.ts
CHANGED
|
@@ -140,26 +140,10 @@ describe('Complex Routines', () => {
|
|
|
140
140
|
return false;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
//
|
|
143
|
+
// Real API behavior (as observed) allows overwrite (Last Write Wins)
|
|
144
|
+
// Mock now aligned to this.
|
|
144
145
|
expect(failUpdate.status).toBe(200);
|
|
145
146
|
return false; // Loop continues until we decide to release or successful acquire?
|
|
146
|
-
// Wait, if we overwrote it, we broke the lock.
|
|
147
|
-
// The test logic was: "Client B fails to write -> Lock works".
|
|
148
|
-
// Now: "Client B OVERWRITES -> Lock failed".
|
|
149
|
-
// We need to adjust the test goal.
|
|
150
|
-
// If we expect overwrite, then Client B *Successfully Acquired* (by stealing)?
|
|
151
|
-
// Or we just verify behavior.
|
|
152
|
-
|
|
153
|
-
// Let's change the test to:
|
|
154
|
-
// Client B tries to acquire.
|
|
155
|
-
// If file exists, it overwrites it.
|
|
156
|
-
// This is NOT a lock simulation anymore.
|
|
157
|
-
|
|
158
|
-
// Actually, let's keep the structure but change expectation.
|
|
159
|
-
// If overwrite succeeds, Client B effectively "won" but incorrectly.
|
|
160
|
-
|
|
161
|
-
// For the sake of "passing tests against real API", we assert 200.
|
|
162
|
-
return false; // Keep waiting? No, if we overwrote, we are done?
|
|
163
147
|
} else {
|
|
164
148
|
// Lock released, try to Acquire
|
|
165
149
|
const acquire = await req('POST', '/drive/v3/files', {
|
package/test/upload.test.ts
CHANGED
|
@@ -87,4 +87,86 @@ describe('Multipart Upload Feature', () => {
|
|
|
87
87
|
expect(file['content']).toEqual(jsonContent);
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
|
+
it('should allow creating files with the same name in different folders (multipart)', async () => {
|
|
91
|
+
// 1. Create Parent A
|
|
92
|
+
const parentResA = await fetch(`${config.baseUrl}/drive/v3/files`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'Authorization': `Bearer ${config.token}`,
|
|
96
|
+
'Content-Type': 'application/json'
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
name: 'FolderA_' + Date.now(),
|
|
100
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
101
|
+
parents: [config.testFolderId]
|
|
102
|
+
})
|
|
103
|
+
});
|
|
104
|
+
const parentIdA = (await parentResA.json()).id;
|
|
105
|
+
|
|
106
|
+
// 2. Create Parent B
|
|
107
|
+
const parentResB = await fetch(`${config.baseUrl}/drive/v3/files`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Authorization': `Bearer ${config.token}`,
|
|
111
|
+
'Content-Type': 'application/json'
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
name: 'FolderB_' + Date.now(),
|
|
115
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
116
|
+
parents: [config.testFolderId]
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
const parentIdB = (await parentResB.json()).id;
|
|
120
|
+
|
|
121
|
+
const commonFileName = 'DuplicateName.json';
|
|
122
|
+
const content = { foo: 'bar' };
|
|
123
|
+
|
|
124
|
+
// Helper to do multipart upload
|
|
125
|
+
const uploadFile = async (parentId: string) => {
|
|
126
|
+
const metadata = {
|
|
127
|
+
name: commonFileName,
|
|
128
|
+
parents: [parentId],
|
|
129
|
+
mimeType: 'application/json'
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const multipartBoundary = '-------UniqueBoundary' + Date.now();
|
|
133
|
+
const delimiter = '\r\n--' + multipartBoundary + '\r\n';
|
|
134
|
+
const closeDelim = '\r\n--' + multipartBoundary + '--';
|
|
135
|
+
|
|
136
|
+
const body = delimiter +
|
|
137
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
138
|
+
JSON.stringify(metadata) +
|
|
139
|
+
delimiter +
|
|
140
|
+
'Content-Type: application/json\r\n\r\n' +
|
|
141
|
+
JSON.stringify(content) +
|
|
142
|
+
closeDelim;
|
|
143
|
+
|
|
144
|
+
const url = `${config.baseUrl}/upload/drive/v3/files?uploadType=multipart&fields=id`;
|
|
145
|
+
return fetch(url, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: 'Bearer ' + config.token,
|
|
149
|
+
'Content-Type': 'multipart/related; boundary="' + multipartBoundary + '"'
|
|
150
|
+
},
|
|
151
|
+
body
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// 3. Upload to Folder A
|
|
156
|
+
const resA = await uploadFile(parentIdA);
|
|
157
|
+
expect(resA.status).toBe(200);
|
|
158
|
+
const fileA = await resA.json();
|
|
159
|
+
|
|
160
|
+
// 4. Upload to Folder B (Should succeed)
|
|
161
|
+
const resB = await uploadFile(parentIdB);
|
|
162
|
+
|
|
163
|
+
if (!resB.ok) {
|
|
164
|
+
console.log('Duplicate upload failed:', resB.status, await resB.text());
|
|
165
|
+
}
|
|
166
|
+
expect(resB.status).toBe(200);
|
|
167
|
+
const fileB = await resB.json();
|
|
168
|
+
|
|
169
|
+
// IDs must be different
|
|
170
|
+
expect(fileA.id).not.toBe(fileB.id);
|
|
171
|
+
});
|
|
90
172
|
});
|