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.
Files changed (48) hide show
  1. package/AGENTS.md +4 -1
  2. package/CLAUDE.md +17 -0
  3. package/dist/index.js +2 -1
  4. package/dist/mappers.d.ts +5 -0
  5. package/dist/mappers.js +15 -0
  6. package/dist/routes/v2.js +16 -8
  7. package/dist/routes/v3.js +59 -10
  8. package/dist/store.js +2 -2
  9. package/package.json +4 -4
  10. package/scripts/check-token.ts +107 -38
  11. package/scripts/run-loop.sh +18 -0
  12. package/src/index.ts +2 -1
  13. package/src/mappers.ts +15 -0
  14. package/src/routes/v2.ts +16 -8
  15. package/src/routes/v3.ts +65 -11
  16. package/src/store.ts +2 -2
  17. package/test/advanced_changes.test.ts +76 -29
  18. package/test/advanced_ordering.test.ts +2 -1
  19. package/test/basics.test.ts +34 -58
  20. package/test/batch_and_query.test.ts +28 -62
  21. package/test/batch_insert_download.test.ts +2 -1
  22. package/test/check_empty.test.ts +60 -0
  23. package/test/complex_query.test.ts +2 -1
  24. package/test/concurrent_fetch.test.ts +2 -1
  25. package/test/config.ts +164 -7
  26. package/test/dates_and_sorting.test.ts +2 -1
  27. package/test/etag.test.ts +8 -4
  28. package/test/features.test.ts +2 -1
  29. package/test/folder_query.test.ts +2 -1
  30. package/test/folder_search.test.ts +2 -1
  31. package/test/iterate_changes.test.ts +175 -74
  32. package/test/latency.test.ts +2 -1
  33. package/test/mime_types.test.ts +2 -1
  34. package/test/missing_fields.test.ts +2 -1
  35. package/test/multipart_behavior.test.ts +2 -1
  36. package/test/parallel_update.test.ts +2 -1
  37. package/test/parity_media_download.test.ts +2 -1
  38. package/test/routines.test.ts +15 -12
  39. package/test/upload.test.ts +2 -1
  40. package/test/url_parameters.test.ts +2 -1
  41. package/test/v2_basics.test.ts +22 -13
  42. package/test/v2_content.test.ts +2 -1
  43. package/test/v2_missing_ops.test.ts +75 -75
  44. package/test/v2_routes.test.ts +31 -21
  45. package/test/v2_upload.test.ts +17 -9
  46. package/test/v3_parity.test.ts +60 -18
  47. package/test_etag_headers.ts +92 -0
  48. package/vitest.config.ts +7 -1
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
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 Content Updates', () => {
@@ -1,13 +1,46 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
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: 'Test Folder V2 Ops',
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: 'Test File V2 Ops',
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
- it('should generate IDs', async () => {
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 res = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}/export?mimeType=text/plain`, {
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(res.status).toBe(400);
90
+ expect(resExport.status).toBe(400);
88
91
  } else {
89
- expect(res.status).toBe(200);
90
- const content = await res.text();
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
- it('should watch file changes', async () => {
96
- const res = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}/watch`, {
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()}`, // Unique ID for real API
105
+ id: `channel-${Date.now()}`,
104
106
  type: 'web_hook',
105
107
  address: 'https://example.com/webhook'
106
108
  })
107
109
  });
108
- expect(res.status).toBe(200);
109
- const data = await res.json();
110
- expect(data.kind).toBe('api#channel');
111
- // Real API returns a different opaque ID, Mock returns fileId.
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
- it('should add parents via update (PUT)', async () => {
116
- const res = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}?addParents=${folderId}`, {
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(res.status).toBe(200);
127
- const data = await res.json();
128
- expect(data.parents.some((p: { id: string }) => p.id === folderId)).toBe(true);
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
- it('should remove parents via update (PUT)', async () => {
132
- const res = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}?removeParents=${folderId}`, {
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(res.status).toBe(200);
141
- const data = await res.json();
142
- const parents = data.parents || [];
143
- expect(parents.some((p: { id: string }) => p.id === folderId)).toBe(false);
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
- it('should add/remove parents via patch (PATCH)', async () => {
147
- // Add
148
- let res = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}?addParents=${folderId}`, {
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
- let data = await res.json();
157
- expect(data.parents.some((p: { id: string }) => p.id === folderId)).toBe(true);
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
- // Remove
160
- res = await fetch(`${config.baseUrl}/drive/v2/files/${fileId}?removeParents=${folderId}`, {
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
- data = await res.json();
169
- const parents = data.parents || [];
170
- expect(parents.some((p: { id: string }) => p.id === folderId)).toBe(false);
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
  });
@@ -1,4 +1,5 @@
1
- import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest';
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 createRes = await req('POST', '/drive/v2/files', { title: 'Test File List', mimeType: 'text/plain' });
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 === 'Test File List')).toBeDefined();
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 metadata = { title: 'Multipart V2', mimeType: 'text/plain' };
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('Multipart V2');
109
+ expect(file.title).toBe(title);
111
110
  });
112
111
 
113
112
  it('should trash file (V2)', async () => {
114
- const file = await createFile('Trash Me');
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 file = await createFile('Copy Me');
123
- const copyRes = await req('POST', `/drive/v2/files/${file.id}/copy`, { title: 'Copied File' });
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('Copied File');
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 file = await createFile('Touch Me');
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
- await createFile('Change Me');
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 file = await createFile('Untrash Me');
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 file1 = await createFile('Trash 1');
159
- const file2 = await createFile('Trash 2');
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 folder = await createFile('Parent Folder', 'application/vnd.google-apps.folder');
200
- const file = await createFile('Child File');
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 file = await createFile('Revision File');
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();
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
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: 'Initial 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: 'Original Multipart 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: 'Updated Multipart 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('Updated Multipart Title');
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: 'ETag Media Test', mimeType: 'text/plain' })
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: 'ETag Multipart Test', mimeType: 'text/plain' })
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: 'Updated 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('Updated Title');
247
+ expect((await correctEtagRes.json()).title).toBe(updatedTitle);
240
248
  });
241
249
  });
@@ -1,4 +1,5 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
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
 
@@ -62,35 +63,76 @@ describe('Google Drive API V3 Parity', () => {
62
63
  return makeRequest(config.target, method, path, headers, body);
63
64
  }
64
65
 
65
- it('should return 400 if fields=etag is requested', async () => {
66
+ it('should return 400 if fields=etag is requested on get', async () => {
66
67
  // Create file
67
- const createRes = await req('POST', '/drive/v3/files', { name: 'V3 Fields Test' });
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
- // Request with fields=etag
71
- const getRes = await req('GET', `/drive/v3/files/${fileId}?fields=etag,name`);
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
+ }
79
+ });
72
80
 
81
+ it('should return 400 if fields=etag is requested on list', async () => {
82
+ const getRes = await req('GET', `/drive/v3/files?fields=files(id,name,mimeType,parents,modifiedTime,size,etag)`);
73
83
  expect(getRes.status).toBe(400);
74
84
  });
75
85
 
76
86
  it('should ignore If-Match header on PATCH (Last Write Wins)', async () => {
77
87
  // Create file
78
- const createRes = await req('POST', '/drive/v3/files', { name: 'V3 If-Match Test' });
88
+ const name = `V3 If-Match Test ${Math.random().toString(36).substring(7)}`;
89
+ const createRes = await req('POST', '/drive/v3/files', { name });
79
90
  const fileId = createRes.body.id;
80
91
 
81
- // Update with Wrong ETag
82
- const updateRes = await req('PATCH', `/drive/v3/files/${fileId}`, {
83
- name: 'Updated Name V3'
84
- }, {
85
- 'If-Match': '"wrong-etag"'
86
- });
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
+ });
87
111
 
88
- // Should Succeed (200) and Update
89
- expect(updateRes.status).toBe(200);
90
- expect(updateRes.body.name).toBe('Updated Name V3');
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;
91
118
 
92
- // Verify update persisted
93
- const getRes = await req('GET', `/drive/v3/files/${fileId}`);
94
- expect(getRes.body.name).toBe('Updated Name V3');
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
+ }
95
137
  });
96
138
  });