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
|
@@ -1,13 +1,46 @@
|
|
|
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
|
|
|
4
5
|
describe('Google Drive V2 Missing Operations', () => {
|
|
5
6
|
let config: TestConfig;
|
|
6
|
-
let fileId: string;
|
|
7
|
-
let folderId: string;
|
|
8
7
|
|
|
9
8
|
beforeAll(async () => {
|
|
10
9
|
config = await getTestConfig();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
if (config) config.stop();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const fetchWithRetry = async (url: string, options: RequestInit, retries = 4, delay = 2000): Promise<Response> => {
|
|
17
|
+
for (let i = 0; i < retries; i++) {
|
|
18
|
+
const res = await fetch(url, options);
|
|
19
|
+
if (res.status === 200) return res;
|
|
20
|
+
if (i < retries - 1) {
|
|
21
|
+
console.warn(`Request to ${url} failed with status ${res.status}. Retrying in ${delay}ms...`);
|
|
22
|
+
await new Promise(r => setTimeout(r, delay));
|
|
23
|
+
} else {
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
throw new Error('Unreachable');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
it('should generate IDs', async () => {
|
|
31
|
+
const res = await fetch(`${config.baseUrl}/drive/v2/files/generateIds?maxResults=5&space=drive`, {
|
|
32
|
+
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
33
|
+
});
|
|
34
|
+
expect(res.status).toBe(200);
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
expect(data.kind).toBe('drive#generatedIds');
|
|
37
|
+
expect(data.ids.length).toBe(5);
|
|
38
|
+
expect(data.space).toBe('drive');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should support export, watch, and parent management lifecycles', async () => {
|
|
42
|
+
const folderTitle = 'Test Folder V2 Ops ' + Math.random().toString(36).substring(7);
|
|
43
|
+
const fileTitle = 'Test File V2 Ops ' + Math.random().toString(36).substring(7);
|
|
11
44
|
|
|
12
45
|
// Create a folder to test parent operations
|
|
13
46
|
const folderRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
@@ -17,12 +50,12 @@ describe('Google Drive V2 Missing Operations', () => {
|
|
|
17
50
|
'Content-Type': 'application/json'
|
|
18
51
|
},
|
|
19
52
|
body: JSON.stringify({
|
|
20
|
-
title:
|
|
53
|
+
title: folderTitle,
|
|
21
54
|
mimeType: 'application/vnd.google-apps.folder'
|
|
22
55
|
})
|
|
23
56
|
});
|
|
24
57
|
const folder = await folderRes.json();
|
|
25
|
-
folderId = folder.id;
|
|
58
|
+
const folderId = folder.id;
|
|
26
59
|
|
|
27
60
|
// Create a dummy file
|
|
28
61
|
const fileRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
@@ -32,43 +65,14 @@ describe('Google Drive V2 Missing Operations', () => {
|
|
|
32
65
|
'Content-Type': 'application/json'
|
|
33
66
|
},
|
|
34
67
|
body: JSON.stringify({
|
|
35
|
-
title:
|
|
68
|
+
title: fileTitle,
|
|
36
69
|
mimeType: 'text/plain'
|
|
37
70
|
})
|
|
38
71
|
});
|
|
39
72
|
const file = await fileRes.json();
|
|
40
|
-
fileId = file.id;
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterAll(async () => {
|
|
44
|
-
if (folderId) {
|
|
45
|
-
await fetch(`${config.baseUrl}/drive/v2/files/${folderId}`, {
|
|
46
|
-
method: 'DELETE',
|
|
47
|
-
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
if (fileId) {
|
|
51
|
-
await fetch(`${config.baseUrl}/drive/v2/files/${fileId}`, {
|
|
52
|
-
method: 'DELETE',
|
|
53
|
-
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
});
|
|
73
|
+
const fileId = file.id;
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
const res = await fetch(`${config.baseUrl}/drive/v2/files/generateIds?maxResults=5&space=drive`, {
|
|
60
|
-
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
61
|
-
});
|
|
62
|
-
expect(res.status).toBe(200);
|
|
63
|
-
const data = await res.json();
|
|
64
|
-
expect(data.kind).toBe('drive#generatedIds');
|
|
65
|
-
expect(data.ids.length).toBe(5);
|
|
66
|
-
expect(data.space).toBe('drive');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should export file content', async () => {
|
|
70
|
-
// For mock, export just returns content effectively.
|
|
71
|
-
// First set some content
|
|
75
|
+
// 1. Export file content
|
|
72
76
|
await fetch(`${config.baseUrl}/upload/drive/v2/files/${fileId}?uploadType=media`, {
|
|
73
77
|
method: 'PUT',
|
|
74
78
|
headers: {
|
|
@@ -78,42 +82,38 @@ describe('Google Drive V2 Missing Operations', () => {
|
|
|
78
82
|
body: 'Hello Export World'
|
|
79
83
|
});
|
|
80
84
|
|
|
81
|
-
const
|
|
85
|
+
const resExport = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}/export?mimeType=text/plain`, {
|
|
82
86
|
headers: { 'Authorization': `Bearer ${config.token}` }
|
|
83
87
|
});
|
|
84
88
|
|
|
85
|
-
// Real API returns 400 for non-Google Docs. Mock returns 200.
|
|
86
89
|
if (config.baseUrl.includes('googleapis')) {
|
|
87
|
-
expect(
|
|
90
|
+
expect(resExport.status).toBe(400);
|
|
88
91
|
} else {
|
|
89
|
-
expect(
|
|
90
|
-
const content = await
|
|
92
|
+
expect(resExport.status).toBe(200);
|
|
93
|
+
const content = await resExport.text();
|
|
91
94
|
expect(content).toBe('Hello Export World');
|
|
92
95
|
}
|
|
93
|
-
});
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
+
// 2. Watch file changes
|
|
98
|
+
const resWatch = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}/watch`, {
|
|
97
99
|
method: 'POST',
|
|
98
100
|
headers: {
|
|
99
101
|
'Authorization': `Bearer ${config.token}`,
|
|
100
102
|
'Content-Type': 'application/json'
|
|
101
103
|
},
|
|
102
104
|
body: JSON.stringify({
|
|
103
|
-
id: `channel-${Date.now()}`,
|
|
105
|
+
id: `channel-${Date.now()}`,
|
|
104
106
|
type: 'web_hook',
|
|
105
107
|
address: 'https://example.com/webhook'
|
|
106
108
|
})
|
|
107
109
|
});
|
|
108
|
-
expect(
|
|
109
|
-
const
|
|
110
|
-
expect(
|
|
111
|
-
|
|
112
|
-
expect(data.resourceId).toBeDefined();
|
|
113
|
-
});
|
|
110
|
+
expect(resWatch.status).toBe(200);
|
|
111
|
+
const watchData = await resWatch.json();
|
|
112
|
+
expect(watchData.kind).toBe('api#channel');
|
|
113
|
+
expect(watchData.resourceId).toBeDefined();
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
const
|
|
115
|
+
// 3. Add parents via update (PUT)
|
|
116
|
+
const resAddPut = await fetchWithRetry(`${config.baseUrl}/drive/v2/files/${fileId}?addParents=${folderId}`, {
|
|
117
117
|
method: 'PUT',
|
|
118
118
|
headers: {
|
|
119
119
|
'Authorization': `Bearer ${config.token}`,
|
|
@@ -123,13 +123,12 @@ describe('Google Drive V2 Missing Operations', () => {
|
|
|
123
123
|
title: 'Updated Title'
|
|
124
124
|
})
|
|
125
125
|
});
|
|
126
|
-
expect(
|
|
127
|
-
const
|
|
128
|
-
expect(
|
|
129
|
-
});
|
|
126
|
+
expect(resAddPut.status).toBe(200);
|
|
127
|
+
const addPutData = await resAddPut.json();
|
|
128
|
+
expect(addPutData.parents.some((p: { id: string }) => p.id === folderId)).toBe(true);
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
const
|
|
130
|
+
// 4. Remove parents via update (PUT)
|
|
131
|
+
const resRemovePut = await fetchWithRetry(`${config.baseUrl}/drive/v2/files/${fileId}?removeParents=${folderId}`, {
|
|
133
132
|
method: 'PUT',
|
|
134
133
|
headers: {
|
|
135
134
|
'Authorization': `Bearer ${config.token}`,
|
|
@@ -137,15 +136,14 @@ describe('Google Drive V2 Missing Operations', () => {
|
|
|
137
136
|
},
|
|
138
137
|
body: JSON.stringify({})
|
|
139
138
|
});
|
|
140
|
-
expect(
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
expect(
|
|
144
|
-
});
|
|
139
|
+
expect(resRemovePut.status).toBe(200);
|
|
140
|
+
const removePutData = await resRemovePut.json();
|
|
141
|
+
const parentsPut = removePutData.parents || [];
|
|
142
|
+
expect(parentsPut.some((p: { id: string }) => p.id === folderId)).toBe(false);
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
// 5. Add/remove parents via patch (PATCH)
|
|
145
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
146
|
+
const resAddPatch = await fetchWithRetry(`${config.baseUrl}/drive/v2/files/${fileId}?addParents=${folderId}`, {
|
|
149
147
|
method: 'PATCH',
|
|
150
148
|
headers: {
|
|
151
149
|
'Authorization': `Bearer ${config.token}`,
|
|
@@ -153,11 +151,12 @@ describe('Google Drive V2 Missing Operations', () => {
|
|
|
153
151
|
},
|
|
154
152
|
body: JSON.stringify({})
|
|
155
153
|
});
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
expect(resAddPatch.status).toBe(200);
|
|
155
|
+
const addPatchData = await resAddPatch.json();
|
|
156
|
+
expect(addPatchData.parents.some((p: { id: string }) => p.id === folderId)).toBe(true);
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
159
|
+
const resRemovePatch = await fetchWithRetry(`${config.baseUrl}/drive/v2/files/${fileId}?removeParents=${folderId}`, {
|
|
161
160
|
method: 'PATCH',
|
|
162
161
|
headers: {
|
|
163
162
|
'Authorization': `Bearer ${config.token}`,
|
|
@@ -165,8 +164,9 @@ describe('Google Drive V2 Missing Operations', () => {
|
|
|
165
164
|
},
|
|
166
165
|
body: JSON.stringify({})
|
|
167
166
|
});
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
167
|
+
expect(resRemovePatch.status).toBe(200);
|
|
168
|
+
const removePatchData = await resRemovePatch.json();
|
|
169
|
+
const parentsPatch = removePatchData.parents || [];
|
|
170
|
+
expect(parentsPatch.some((p: { id: string }) => p.id === folderId)).toBe(false);
|
|
171
171
|
});
|
|
172
172
|
});
|
package/test/v2_routes.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, beforeAll, afterAll, expect } from 'vitest';
|
|
2
|
+
import { it } from './config';;
|
|
2
3
|
import { getTestConfig, TestConfig } from './config';
|
|
3
4
|
|
|
4
5
|
describe('Google Drive V2 Routes', () => {
|
|
@@ -12,10 +13,6 @@ describe('Google Drive V2 Routes', () => {
|
|
|
12
13
|
if (config) config.stop();
|
|
13
14
|
});
|
|
14
15
|
|
|
15
|
-
beforeEach(async () => {
|
|
16
|
-
if (config) await config.clear();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
16
|
function getBaseUrl() {
|
|
20
17
|
if (typeof config.target === 'string') {
|
|
21
18
|
return config.target;
|
|
@@ -56,7 +53,8 @@ describe('Google Drive V2 Routes', () => {
|
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
it('should list files (V2)', async () => {
|
|
59
|
-
const
|
|
56
|
+
const title = 'Test File List ' + Math.random().toString(36).substring(7);
|
|
57
|
+
const createRes = await req('POST', '/drive/v2/files', { title, mimeType: 'text/plain' });
|
|
60
58
|
expect(createRes.status).toBe(200);
|
|
61
59
|
|
|
62
60
|
const listRes = await req('GET', '/drive/v2/files');
|
|
@@ -66,7 +64,7 @@ describe('Google Drive V2 Routes', () => {
|
|
|
66
64
|
expect(listData.items).toBeDefined();
|
|
67
65
|
expect(Array.isArray(listData.items)).toBe(true);
|
|
68
66
|
expect(listData.items.length).toBeGreaterThan(0);
|
|
69
|
-
expect(listData.items.find((f: { title: string }) => f.title ===
|
|
67
|
+
expect(listData.items.find((f: { title: string }) => f.title === title)).toBeDefined();
|
|
70
68
|
});
|
|
71
69
|
|
|
72
70
|
it('should get about info (V2)', async () => {
|
|
@@ -79,7 +77,8 @@ describe('Google Drive V2 Routes', () => {
|
|
|
79
77
|
});
|
|
80
78
|
|
|
81
79
|
it('should upload file via multipart (V2)', async () => {
|
|
82
|
-
const
|
|
80
|
+
const title = 'Multipart V2 ' + Math.random().toString(36).substring(7);
|
|
81
|
+
const metadata = { title, mimeType: 'text/plain' };
|
|
83
82
|
const content = { foo: 'bar' };
|
|
84
83
|
|
|
85
84
|
const boundary = '-------314159265358979323846';
|
|
@@ -107,11 +106,12 @@ describe('Google Drive V2 Routes', () => {
|
|
|
107
106
|
|
|
108
107
|
expect(res.status).toBe(200);
|
|
109
108
|
const file = await res.json();
|
|
110
|
-
expect(file.title).toBe(
|
|
109
|
+
expect(file.title).toBe(title);
|
|
111
110
|
});
|
|
112
111
|
|
|
113
112
|
it('should trash file (V2)', async () => {
|
|
114
|
-
const
|
|
113
|
+
const title = 'Trash Me ' + Math.random().toString(36).substring(7);
|
|
114
|
+
const file = await createFile(title);
|
|
115
115
|
const trashRes = await req('POST', `/drive/v2/files/${file.id}/trash`);
|
|
116
116
|
expect(trashRes.status).toBe(200);
|
|
117
117
|
const trashedFile = await trashRes.json();
|
|
@@ -119,16 +119,19 @@ describe('Google Drive V2 Routes', () => {
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
it('should copy file (V2)', async () => {
|
|
122
|
-
const
|
|
123
|
-
const
|
|
122
|
+
const title = 'Copy Me ' + Math.random().toString(36).substring(7);
|
|
123
|
+
const copyTitle = 'Copied File ' + Math.random().toString(36).substring(7);
|
|
124
|
+
const file = await createFile(title);
|
|
125
|
+
const copyRes = await req('POST', `/drive/v2/files/${file.id}/copy`, { title: copyTitle });
|
|
124
126
|
expect(copyRes.status).toBe(200);
|
|
125
127
|
const copiedFile = await copyRes.json();
|
|
126
|
-
expect(copiedFile.title).toBe(
|
|
128
|
+
expect(copiedFile.title).toBe(copyTitle);
|
|
127
129
|
expect(copiedFile.id).not.toBe(file.id);
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
it('should touch file (V2)', async () => {
|
|
131
|
-
const
|
|
133
|
+
const title = 'Touch Me ' + Math.random().toString(36).substring(7);
|
|
134
|
+
const file = await createFile(title);
|
|
132
135
|
const touchRes = await req('POST', `/drive/v2/files/${file.id}/touch`);
|
|
133
136
|
expect(touchRes.status).toBe(200);
|
|
134
137
|
const touchedFile = await touchRes.json();
|
|
@@ -136,7 +139,8 @@ describe('Google Drive V2 Routes', () => {
|
|
|
136
139
|
});
|
|
137
140
|
|
|
138
141
|
it('should list changes (V2)', async () => {
|
|
139
|
-
|
|
142
|
+
const title = 'Change Me ' + Math.random().toString(36).substring(7);
|
|
143
|
+
await createFile(title);
|
|
140
144
|
const changesRes = await req('GET', '/drive/v2/changes');
|
|
141
145
|
expect(changesRes.status).toBe(200);
|
|
142
146
|
const changesData = await changesRes.json();
|
|
@@ -145,7 +149,8 @@ describe('Google Drive V2 Routes', () => {
|
|
|
145
149
|
});
|
|
146
150
|
|
|
147
151
|
it('should untrash file (V2)', async () => {
|
|
148
|
-
const
|
|
152
|
+
const title = 'Untrash Me ' + Math.random().toString(36).substring(7);
|
|
153
|
+
const file = await createFile(title);
|
|
149
154
|
await req('POST', `/drive/v2/files/${file.id}/trash`);
|
|
150
155
|
|
|
151
156
|
const untrashRes = await req('POST', `/drive/v2/files/${file.id}/untrash`);
|
|
@@ -155,8 +160,10 @@ describe('Google Drive V2 Routes', () => {
|
|
|
155
160
|
});
|
|
156
161
|
|
|
157
162
|
it('should empty trash (V2)', async () => {
|
|
158
|
-
const
|
|
159
|
-
const
|
|
163
|
+
const title1 = 'Trash 1 ' + Math.random().toString(36).substring(7);
|
|
164
|
+
const title2 = 'Trash 2 ' + Math.random().toString(36).substring(7);
|
|
165
|
+
const file1 = await createFile(title1);
|
|
166
|
+
const file2 = await createFile(title2);
|
|
160
167
|
await req('POST', `/drive/v2/files/${file1.id}/trash`);
|
|
161
168
|
await req('POST', `/drive/v2/files/${file2.id}/trash`);
|
|
162
169
|
|
|
@@ -196,8 +203,10 @@ describe('Google Drive V2 Routes', () => {
|
|
|
196
203
|
|
|
197
204
|
it('should manage parents (V2)', async () => {
|
|
198
205
|
// Create folder
|
|
199
|
-
const
|
|
200
|
-
const
|
|
206
|
+
const folderTitle = 'Parent Folder ' + Math.random().toString(36).substring(7);
|
|
207
|
+
const childTitle = 'Child File ' + Math.random().toString(36).substring(7);
|
|
208
|
+
const folder = await createFile(folderTitle, 'application/vnd.google-apps.folder');
|
|
209
|
+
const file = await createFile(childTitle);
|
|
201
210
|
|
|
202
211
|
// Insert parent
|
|
203
212
|
const insertRes = await req('POST', `/drive/v2/files/${file.id}/parents`, { id: folder.id });
|
|
@@ -223,7 +232,8 @@ describe('Google Drive V2 Routes', () => {
|
|
|
223
232
|
});
|
|
224
233
|
|
|
225
234
|
it('should get revisions (V2)', async () => {
|
|
226
|
-
const
|
|
235
|
+
const title = 'Revision File ' + Math.random().toString(36).substring(7);
|
|
236
|
+
const file = await createFile(title);
|
|
227
237
|
const listRes = await req('GET', `/drive/v2/files/${file.id}/revisions`);
|
|
228
238
|
expect(listRes.status).toBe(200);
|
|
229
239
|
const listData = await listRes.json();
|
package/test/v2_upload.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
|
|
|
4
5
|
describe('V2 Upload Features', () => {
|
|
@@ -41,6 +42,7 @@ describe('V2 Upload Features', () => {
|
|
|
41
42
|
|
|
42
43
|
it('should update a file content using PUT /upload/drive/v2/files/:fileId?uploadType=media', async () => {
|
|
43
44
|
// 1. Create a file normally first
|
|
45
|
+
const title = 'Initial Title ' + Math.random().toString(36).substring(7);
|
|
44
46
|
const createRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
45
47
|
method: 'POST',
|
|
46
48
|
headers: {
|
|
@@ -48,7 +50,7 @@ describe('V2 Upload Features', () => {
|
|
|
48
50
|
'Content-Type': 'application/json'
|
|
49
51
|
},
|
|
50
52
|
body: JSON.stringify({
|
|
51
|
-
title
|
|
53
|
+
title,
|
|
52
54
|
mimeType: 'text/plain'
|
|
53
55
|
})
|
|
54
56
|
});
|
|
@@ -85,6 +87,7 @@ describe('V2 Upload Features', () => {
|
|
|
85
87
|
|
|
86
88
|
it('should update metadata and content using PUT /upload/drive/v2/files/:fileId?uploadType=multipart', async () => {
|
|
87
89
|
// 1. Create a file
|
|
90
|
+
const origTitle = 'Original Multipart Title ' + Math.random().toString(36).substring(7);
|
|
88
91
|
const createRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
89
92
|
method: 'POST',
|
|
90
93
|
headers: {
|
|
@@ -92,7 +95,7 @@ describe('V2 Upload Features', () => {
|
|
|
92
95
|
'Content-Type': 'application/json'
|
|
93
96
|
},
|
|
94
97
|
body: JSON.stringify({
|
|
95
|
-
title:
|
|
98
|
+
title: origTitle,
|
|
96
99
|
mimeType: 'text/html'
|
|
97
100
|
})
|
|
98
101
|
});
|
|
@@ -104,8 +107,9 @@ describe('V2 Upload Features', () => {
|
|
|
104
107
|
const delimiter = `\r\n--${boundary}\r\n`;
|
|
105
108
|
const closeDelim = `\r\n--${boundary}--`;
|
|
106
109
|
|
|
110
|
+
const newTitle = 'Updated Multipart Title ' + Math.random().toString(36).substring(7);
|
|
107
111
|
const metadata = {
|
|
108
|
-
title:
|
|
112
|
+
title: newTitle,
|
|
109
113
|
mimeType: 'text/plain'
|
|
110
114
|
};
|
|
111
115
|
const newContent = 'Updated Multipart Content';
|
|
@@ -131,7 +135,7 @@ describe('V2 Upload Features', () => {
|
|
|
131
135
|
const updatedFile = await updateRes.json();
|
|
132
136
|
|
|
133
137
|
// 3. Verify updates
|
|
134
|
-
expect(updatedFile.title).toBe(
|
|
138
|
+
expect(updatedFile.title).toBe(newTitle);
|
|
135
139
|
expect(updatedFile.mimeType).toBe('text/plain');
|
|
136
140
|
|
|
137
141
|
const contentRes = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}?alt=media`, {
|
|
@@ -139,15 +143,17 @@ describe('V2 Upload Features', () => {
|
|
|
139
143
|
});
|
|
140
144
|
expect(await contentRes.text()).toBe(newContent);
|
|
141
145
|
});
|
|
146
|
+
|
|
142
147
|
it('should respect If-Match header in V2 media upload (PUT)', async () => {
|
|
143
148
|
// 1. Create file
|
|
149
|
+
const title = 'ETag Media Test ' + Math.random().toString(36).substring(7);
|
|
144
150
|
const createRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
145
151
|
method: 'POST',
|
|
146
152
|
headers: {
|
|
147
153
|
'Authorization': `Bearer ${config.token}`,
|
|
148
154
|
'Content-Type': 'application/json'
|
|
149
155
|
},
|
|
150
|
-
body: JSON.stringify({ title
|
|
156
|
+
body: JSON.stringify({ title, mimeType: 'text/plain' })
|
|
151
157
|
});
|
|
152
158
|
const file = await createRes.json();
|
|
153
159
|
const fileId = file.id;
|
|
@@ -188,24 +194,26 @@ describe('V2 Upload Features', () => {
|
|
|
188
194
|
|
|
189
195
|
it('should respect If-Match header in V2 multipart upload (PUT)', async () => {
|
|
190
196
|
// 1. Create file
|
|
197
|
+
const title = 'ETag Multipart Test ' + Math.random().toString(36).substring(7);
|
|
191
198
|
const createRes = await fetch(`${config.baseUrl}/drive/v2/files`, {
|
|
192
199
|
method: 'POST',
|
|
193
200
|
headers: {
|
|
194
201
|
'Authorization': `Bearer ${config.token}`,
|
|
195
202
|
'Content-Type': 'application/json'
|
|
196
203
|
},
|
|
197
|
-
body: JSON.stringify({ title
|
|
204
|
+
body: JSON.stringify({ title, mimeType: 'text/plain' })
|
|
198
205
|
});
|
|
199
206
|
const file = await createRes.json();
|
|
200
207
|
const fileId = file.id;
|
|
201
208
|
const etag = file.etag;
|
|
202
209
|
|
|
210
|
+
const updatedTitle = 'Updated Title ' + Math.random().toString(36).substring(7);
|
|
203
211
|
const boundary = 'foo_bar_baz';
|
|
204
212
|
const delimiter = `\r\n--${boundary}\r\n`;
|
|
205
213
|
const closeDelim = `\r\n--${boundary}--`;
|
|
206
214
|
const body = delimiter +
|
|
207
215
|
'Content-Type: application/json\r\n\r\n' +
|
|
208
|
-
JSON.stringify({ title:
|
|
216
|
+
JSON.stringify({ title: updatedTitle }) +
|
|
209
217
|
delimiter +
|
|
210
218
|
'Content-Type: text/plain\r\n\r\n' +
|
|
211
219
|
'Multipart Update' +
|
|
@@ -236,6 +244,6 @@ describe('V2 Upload Features', () => {
|
|
|
236
244
|
body: body
|
|
237
245
|
});
|
|
238
246
|
expect(correctEtagRes.status).toBe(200);
|
|
239
|
-
expect((await correctEtagRes.json()).title).toBe(
|
|
247
|
+
expect((await correctEtagRes.json()).title).toBe(updatedTitle);
|
|
240
248
|
});
|
|
241
249
|
});
|
package/test/v3_parity.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
|
|
|
@@ -64,39 +65,74 @@ describe('Google Drive API V3 Parity', () => {
|
|
|
64
65
|
|
|
65
66
|
it('should return 400 if fields=etag is requested on get', async () => {
|
|
66
67
|
// Create file
|
|
67
|
-
const
|
|
68
|
+
const name = `V3 Fields Test ${Math.random().toString(36).substring(7)}`;
|
|
69
|
+
const createRes = await req('POST', '/drive/v3/files', { name });
|
|
68
70
|
const fileId = createRes.body.id;
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
try {
|
|
73
|
+
// Request with fields=etag
|
|
74
|
+
const getRes = await req('GET', `/drive/v3/files/${fileId}?fields=etag,name`);
|
|
75
|
+
expect(getRes.status).toBe(400);
|
|
76
|
+
} finally {
|
|
77
|
+
await req('DELETE', `/drive/v3/files/${fileId}`);
|
|
78
|
+
}
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
it('should return 400 if fields=etag is requested on list', async () => {
|
|
77
82
|
const getRes = await req('GET', `/drive/v3/files?fields=files(id,name,mimeType,parents,modifiedTime,size,etag)`);
|
|
78
|
-
|
|
79
83
|
expect(getRes.status).toBe(400);
|
|
80
84
|
});
|
|
81
85
|
|
|
82
86
|
it('should ignore If-Match header on PATCH (Last Write Wins)', async () => {
|
|
83
87
|
// Create file
|
|
84
|
-
const
|
|
88
|
+
const name = `V3 If-Match Test ${Math.random().toString(36).substring(7)}`;
|
|
89
|
+
const createRes = await req('POST', '/drive/v3/files', { name });
|
|
85
90
|
const fileId = createRes.body.id;
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
try {
|
|
93
|
+
// Update with Wrong ETag
|
|
94
|
+
const updateRes = await req('PATCH', `/drive/v3/files/${fileId}`, {
|
|
95
|
+
name: 'Updated Name V3'
|
|
96
|
+
}, {
|
|
97
|
+
'If-Match': '"wrong-etag"'
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Should Succeed (200) and Update
|
|
101
|
+
expect(updateRes.status).toBe(200);
|
|
102
|
+
expect(updateRes.body.name).toBe('Updated Name V3');
|
|
103
|
+
|
|
104
|
+
// Verify update persisted
|
|
105
|
+
const getRes = await req('GET', `/drive/v3/files/${fileId}`);
|
|
106
|
+
expect(getRes.body.name).toBe('Updated Name V3');
|
|
107
|
+
} finally {
|
|
108
|
+
await req('DELETE', `/drive/v3/files/${fileId}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
it('should allow fetching ETag from V2 and using it for If-Match content updates (RxDB replication flow)', async () => {
|
|
113
|
+
// 1. Create file via POST
|
|
114
|
+
const name = `V3 ETag Header Test ${Math.random().toString(36).substring(7)}`;
|
|
115
|
+
const createRes = await req('POST', '/drive/v3/files', { name });
|
|
116
|
+
expect(createRes.status).toBe(200);
|
|
117
|
+
const fileId = createRes.body.id;
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
try {
|
|
120
|
+
// 2. Fetch the file via V2 GET to obtain the ETag
|
|
121
|
+
const v2Res = await req('GET', `/drive/v2/files/${fileId}`);
|
|
122
|
+
expect(v2Res.status).toBe(200);
|
|
123
|
+
const etag = v2Res.body.etag;
|
|
124
|
+
expect(etag).toBeDefined();
|
|
125
|
+
expect(etag).toBeTruthy();
|
|
126
|
+
|
|
127
|
+
// 3. Update via V3 PATCH upload with If-Match: etag (representing RxDB's V3 updates with V2-obtained ETag)
|
|
128
|
+
const updateRes = await req('PATCH', `/upload/drive/v3/files/${fileId}?uploadType=media`, 'v3 content', {
|
|
129
|
+
'Content-Type': 'text/plain',
|
|
130
|
+
'If-Match': etag
|
|
131
|
+
});
|
|
132
|
+
expect(updateRes.status).toBe(200);
|
|
133
|
+
} finally {
|
|
134
|
+
// Clean up: delete file
|
|
135
|
+
await req('DELETE', `/drive/v3/files/${fileId}`);
|
|
136
|
+
}
|
|
101
137
|
});
|
|
102
138
|
});
|