google-drive-mock 1.1.0 → 1.1.2

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.
@@ -18,6 +18,11 @@ When working on this project, always adhere to the following workflow to ensure
18
18
  - Run this **AFTER** verifying the tests against the Real API.
19
19
  - This ensures that the Mock server implementation correctly handles the now-verified tests.
20
20
 
21
+ 4. **Ensure build works**
22
+ - Run "npm run lint"
23
+ - Run "npm run build"
24
+
25
+
21
26
 
22
27
  No Goes:
23
28
 
@@ -4,7 +4,7 @@ on:
4
4
  workflow_dispatch:
5
5
  inputs:
6
6
  version:
7
- description: 'New Version (e.g. 1.0.0, or leave empty for minor)'
7
+ description: 'New Version (e.g. 1.0.0, or leave empty for patch)'
8
8
  required: false
9
9
  type: string
10
10
 
@@ -29,7 +29,7 @@ jobs:
29
29
  id: bump
30
30
  run: |
31
31
  if [ -z "${{ inputs.version }}" ]; then
32
- VERSION=$(npm version minor --no-git-tag-version)
32
+ VERSION=$(npm version patch --no-git-tag-version)
33
33
  else
34
34
  VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)
35
35
  fi
package/dist/index.js CHANGED
@@ -29,6 +29,14 @@ const createApp = (config = {}) => {
29
29
  exposedHeaders: ['ETag', 'Date', 'Content-Length']
30
30
  }));
31
31
  app.set('etag', false); // Disable default ETag generation to match Real API behavior
32
+ // Random delay to simulate real-world network latency (0-20ms)
33
+ app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
34
+ const delay = Math.floor(Math.random() * 21); // 0 to 20ms
35
+ if (delay > 0) {
36
+ yield new Promise(resolve => setTimeout(resolve, delay));
37
+ }
38
+ next();
39
+ }));
32
40
  app.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
33
41
  if (config.serverLagBefore && config.serverLagBefore > 0) {
34
42
  yield new Promise(resolve => setTimeout(resolve, config.serverLagBefore));
package/dist/mappers.d.ts CHANGED
@@ -7,3 +7,11 @@ export declare function toV2File(file: DriveFile): Record<string, unknown>;
7
7
  * Maps a V2 API File Update/Insert body to a partial Internal DriveFile (V3 format).
8
8
  */
9
9
  export declare function fromV2Update(body: Record<string, unknown>): Partial<DriveFile>;
10
+ /**
11
+ * Filters an object based on the Google Drive API `fields` parameter.
12
+ * Supports nested fields like "files(id,name,parents)".
13
+ *
14
+ * @param data The object to filter
15
+ * @param fields The fields selection string
16
+ */
17
+ export declare function applyFields(data: unknown, fields: string): unknown;
package/dist/mappers.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toV2File = toV2File;
4
4
  exports.fromV2Update = fromV2Update;
5
+ exports.applyFields = applyFields;
5
6
  /**
6
7
  * Maps an internal DriveFile (V3 format) to a V2 API File resource.
7
8
  */
@@ -61,3 +62,81 @@ function fromV2Update(body) {
61
62
  }
62
63
  return update;
63
64
  }
65
+ /**
66
+ * Filters an object based on the Google Drive API `fields` parameter.
67
+ * Supports nested fields like "files(id,name,parents)".
68
+ *
69
+ * @param data The object to filter
70
+ * @param fields The fields selection string
71
+ */
72
+ function applyFields(data, fields) {
73
+ if (!fields || fields === '*')
74
+ return data;
75
+ // Parse top-level fields
76
+ const parsedFields = [];
77
+ let depth = 0;
78
+ let currentStart = 0;
79
+ for (let i = 0; i < fields.length; i++) {
80
+ const char = fields[i];
81
+ if (char === '(')
82
+ depth++;
83
+ else if (char === ')')
84
+ depth--;
85
+ else if (char === ',' && depth === 0) {
86
+ // Split
87
+ parseFieldPart(fields.substring(currentStart, i), parsedFields);
88
+ currentStart = i + 1;
89
+ }
90
+ }
91
+ // Last part
92
+ parseFieldPart(fields.substring(currentStart), parsedFields);
93
+ if (Array.isArray(data)) {
94
+ // If data is array, apply to each item?
95
+ // Usually fields selection on array applies to the object WRAPPING the array,
96
+ // e.g. "files(id)" on { files: [...] }.
97
+ // But if we are called RESURSIVELY on an array value, we should map it.
98
+ return data.map(item => applyFields(item, fields));
99
+ }
100
+ if (typeof data !== 'object' || data === null) {
101
+ return data;
102
+ }
103
+ const dataObj = data;
104
+ const result = {};
105
+ for (const field of parsedFields) {
106
+ if (field.key === '*') {
107
+ // Wildcard at this level - copy everything?
108
+ // Logic can be complex. For now, strict parity with requested use case.
109
+ return data;
110
+ }
111
+ if (Object.prototype.hasOwnProperty.call(dataObj, field.key)) {
112
+ const value = dataObj[field.key];
113
+ if (field.subFields) {
114
+ // Recursive apply
115
+ if (Array.isArray(value)) {
116
+ result[field.key] = value.map((item) => applyFields(item, field.subFields));
117
+ }
118
+ else {
119
+ result[field.key] = applyFields(value, field.subFields);
120
+ }
121
+ }
122
+ else {
123
+ result[field.key] = value;
124
+ }
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+ function parseFieldPart(part, result) {
130
+ const trimmed = part.trim();
131
+ if (!trimmed)
132
+ return;
133
+ const parenStart = trimmed.indexOf('(');
134
+ if (parenStart !== -1 && trimmed.endsWith(')')) {
135
+ const key = trimmed.substring(0, parenStart);
136
+ const subFields = trimmed.substring(parenStart + 1, trimmed.length - 1);
137
+ result.push({ key, subFields });
138
+ }
139
+ else {
140
+ result.push({ key: trimmed });
141
+ }
142
+ }
package/dist/routes/v3.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createV3Router = void 0;
7
7
  const express_1 = __importDefault(require("express"));
8
8
  const store_1 = require("../store");
9
+ const mappers_1 = require("../mappers");
9
10
  const createV3Router = () => {
10
11
  const app = express_1.default.Router();
11
12
  // About
@@ -22,7 +23,7 @@ const createV3Router = () => {
22
23
  // Enhanced query parser for Mock
23
24
  // Recursive function to handle nested OR/AND logic with parens
24
25
  const evaluateQuery = (queryStr, file) => {
25
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
26
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
26
27
  const str = queryStr.trim();
27
28
  if (!str)
28
29
  return true;
@@ -140,6 +141,10 @@ const createV3Router = () => {
140
141
  const timeStr = (_l = part.match(/modifiedTime = '(.*)'/)) === null || _l === void 0 ? void 0 : _l[1];
141
142
  return !!(timeStr && new Date(file.modifiedTime).toISOString() === new Date(timeStr).toISOString());
142
143
  }
144
+ if (part.startsWith("modifiedTime >= '")) {
145
+ const timeStr = (_m = part.match(/modifiedTime >= '(.*)'/)) === null || _m === void 0 ? void 0 : _m[1];
146
+ return !!(timeStr && new Date(file.modifiedTime) >= new Date(timeStr));
147
+ }
143
148
  // Fallback / Unknown
144
149
  return true;
145
150
  };
@@ -198,12 +203,19 @@ const createV3Router = () => {
198
203
  const nextSkip = skip + pageSize;
199
204
  nextPageToken = Buffer.from(JSON.stringify({ skip: nextSkip })).toString('base64');
200
205
  }
201
- res.json({
206
+ const response = {
202
207
  kind: "drive#fileList",
203
208
  incompleteSearch: false,
204
209
  files: resultFiles,
205
210
  nextPageToken
206
- });
211
+ };
212
+ const fields = req.query.fields;
213
+ if (fields) {
214
+ res.json((0, mappers_1.applyFields)(response, fields));
215
+ }
216
+ else {
217
+ res.json(response);
218
+ }
207
219
  });
208
220
  // Changes: Get Start Page Token
209
221
  app.get('/drive/v3/changes/startPageToken', (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-drive-mock",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Mock-Server that simulates being google-drive. Used for testing.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -18,6 +18,15 @@ const createApp = (config: AppConfig = {}) => {
18
18
  }));
19
19
  app.set('etag', false); // Disable default ETag generation to match Real API behavior
20
20
 
21
+ // Random delay to simulate real-world network latency (0-20ms)
22
+ app.use(async (req, res, next) => {
23
+ const delay = Math.floor(Math.random() * 21); // 0 to 20ms
24
+ if (delay > 0) {
25
+ await new Promise(resolve => setTimeout(resolve, delay));
26
+ }
27
+ next();
28
+ });
29
+
21
30
  app.use(async (req, res, next) => {
22
31
  if (config.serverLagBefore && config.serverLagBefore > 0) {
23
32
  await new Promise(resolve => setTimeout(resolve, config.serverLagBefore));
package/src/mappers.ts CHANGED
@@ -59,3 +59,87 @@ export function fromV2Update(body: Record<string, unknown>): Partial<DriveFile>
59
59
 
60
60
  return update;
61
61
  }
62
+
63
+
64
+ /**
65
+ * Filters an object based on the Google Drive API `fields` parameter.
66
+ * Supports nested fields like "files(id,name,parents)".
67
+ *
68
+ * @param data The object to filter
69
+ * @param fields The fields selection string
70
+ */
71
+ export function applyFields(data: unknown, fields: string): unknown {
72
+ if (!fields || fields === '*') return data;
73
+
74
+ // Parse top-level fields
75
+ const parsedFields: { key: string, subFields?: string }[] = [];
76
+
77
+ let depth = 0;
78
+ let currentStart = 0;
79
+
80
+ for (let i = 0; i < fields.length; i++) {
81
+ const char = fields[i];
82
+ if (char === '(') depth++;
83
+ else if (char === ')') depth--;
84
+ else if (char === ',' && depth === 0) {
85
+ // Split
86
+ parseFieldPart(fields.substring(currentStart, i), parsedFields);
87
+ currentStart = i + 1;
88
+ }
89
+ }
90
+ // Last part
91
+ parseFieldPart(fields.substring(currentStart), parsedFields);
92
+
93
+ if (Array.isArray(data)) {
94
+ // If data is array, apply to each item?
95
+ // Usually fields selection on array applies to the object WRAPPING the array,
96
+ // e.g. "files(id)" on { files: [...] }.
97
+ // But if we are called RESURSIVELY on an array value, we should map it.
98
+ return data.map(item => applyFields(item, fields));
99
+ }
100
+
101
+ if (typeof data !== 'object' || data === null) {
102
+ return data;
103
+ }
104
+
105
+ const dataObj = data as Record<string, unknown>;
106
+ const result: Record<string, unknown> = {};
107
+
108
+ for (const field of parsedFields) {
109
+ if (field.key === '*') {
110
+ // Wildcard at this level - copy everything?
111
+ // Logic can be complex. For now, strict parity with requested use case.
112
+ return data;
113
+ }
114
+
115
+ if (Object.prototype.hasOwnProperty.call(dataObj, field.key)) {
116
+ const value = dataObj[field.key];
117
+ if (field.subFields) {
118
+ // Recursive apply
119
+ if (Array.isArray(value)) {
120
+ result[field.key] = value.map((item: unknown) => applyFields(item, field.subFields!));
121
+ } else {
122
+ result[field.key] = applyFields(value, field.subFields!);
123
+ }
124
+ } else {
125
+ result[field.key] = value;
126
+ }
127
+ }
128
+ }
129
+
130
+ return result;
131
+ }
132
+
133
+ function parseFieldPart(part: string, result: { key: string, subFields?: string }[]) {
134
+ const trimmed = part.trim();
135
+ if (!trimmed) return;
136
+
137
+ const parenStart = trimmed.indexOf('(');
138
+ if (parenStart !== -1 && trimmed.endsWith(')')) {
139
+ const key = trimmed.substring(0, parenStart);
140
+ const subFields = trimmed.substring(parenStart + 1, trimmed.length - 1);
141
+ result.push({ key, subFields });
142
+ } else {
143
+ result.push({ key: trimmed });
144
+ }
145
+ }
package/src/routes/v3.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import express, { Request, Response } from 'express';
2
2
  import { driveStore } from '../store';
3
+ import { applyFields } from '../mappers';
3
4
 
4
5
  export const createV3Router = () => {
5
6
  const app = express.Router();
@@ -140,6 +141,10 @@ export const createV3Router = () => {
140
141
  const timeStr = part.match(/modifiedTime = '(.*)'/)?.[1];
141
142
  return !!(timeStr && new Date(file.modifiedTime).toISOString() === new Date(timeStr).toISOString());
142
143
  }
144
+ if (part.startsWith("modifiedTime >= '")) {
145
+ const timeStr = part.match(/modifiedTime >= '(.*)'/)?.[1];
146
+ return !!(timeStr && new Date(file.modifiedTime) >= new Date(timeStr));
147
+ }
143
148
 
144
149
  // Fallback / Unknown
145
150
  return true;
@@ -205,12 +210,19 @@ export const createV3Router = () => {
205
210
  nextPageToken = Buffer.from(JSON.stringify({ skip: nextSkip })).toString('base64');
206
211
  }
207
212
 
208
- res.json({
213
+ const response: Record<string, unknown> = {
209
214
  kind: "drive#fileList",
210
215
  incompleteSearch: false,
211
216
  files: resultFiles,
212
217
  nextPageToken
213
- });
218
+ };
219
+
220
+ const fields = req.query.fields as string;
221
+ if (fields) {
222
+ res.json(applyFields(response, fields));
223
+ } else {
224
+ res.json(response);
225
+ }
214
226
  });
215
227
 
216
228
  // Changes: Get Start Page Token
@@ -0,0 +1,177 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { getTestConfig, TestConfig } from './config';
3
+
4
+ const randomString = () => Math.random().toString(36).substring(7);
5
+
6
+ describe('Folder Query Investigation', () => {
7
+ let config: TestConfig;
8
+ let headers: Record<string, string>;
9
+
10
+ beforeAll(async () => {
11
+ config = await getTestConfig();
12
+ headers = {
13
+ Authorization: `Bearer ${config.token}`
14
+ };
15
+ });
16
+
17
+ it('should return files in a folder with specific query parameters', async () => {
18
+ // 1. Create a parent folder
19
+ const parentRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
20
+ method: 'POST',
21
+ headers: { ...headers, 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({
23
+ name: 'QueryTestParams_' + randomString(),
24
+ mimeType: 'application/vnd.google-apps.folder'
25
+ })
26
+ });
27
+ expect(parentRes.status).toBe(200);
28
+ const parentId = (await parentRes.json()).id;
29
+
30
+ // 2. Create a few files in the folder
31
+ const file1 = await fetch(`${config.baseUrl}/drive/v3/files`, {
32
+ method: 'POST',
33
+ headers: { ...headers, 'Content-Type': 'application/json' },
34
+ body: JSON.stringify({
35
+ name: 'file1',
36
+ parents: [parentId]
37
+ })
38
+ }).then(res => res.json());
39
+
40
+ const file2 = await fetch(`${config.baseUrl}/drive/v3/files`, {
41
+ method: 'POST',
42
+ headers: { ...headers, 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({
44
+ name: 'file2',
45
+ parents: [parentId]
46
+ })
47
+ }).then(res => res.json());
48
+
49
+ // Wait for consistency
50
+ await new Promise(r => setTimeout(r, 2000));
51
+
52
+ // 3. Run the user's specific query
53
+ const queryParts = [
54
+ `'${parentId}' in parents`,
55
+ `and trashed = false`
56
+ ];
57
+
58
+ // Optional: Add modifiedTime check if needed, but let's start with basic structure
59
+ // const checkpoint = { modifiedTime: '...' };
60
+ // if (checkpoint) {
61
+ // queryParts.push(`and modifiedTime >= '${checkpoint.modifiedTime}'`);
62
+ // }
63
+
64
+ const batchSize = 10;
65
+ const params = new URLSearchParams({
66
+ q: queryParts.join(' '),
67
+ pageSize: (batchSize + 10) + '',
68
+ orderBy: "modifiedTime asc,name asc",
69
+ fields: "files(id,name,mimeType,parents,modifiedTime,size)",
70
+ supportsAllDrives: "true",
71
+ includeItemsFromAllDrives: "true",
72
+ });
73
+
74
+ const url =
75
+ config.baseUrl +
76
+ "/drive/v3/files?" +
77
+ params.toString();
78
+
79
+ console.log('Requesting URL:', url);
80
+
81
+ const res = await fetch(url, {
82
+ headers: {
83
+ Authorization: `Bearer ${config.token}`,
84
+ },
85
+ });
86
+
87
+ if (res.status !== 200) {
88
+ console.error('Error response:', await res.text());
89
+ }
90
+ expect(res.status).toBe(200);
91
+ const data = await res.json();
92
+
93
+ console.log('Found files:', data.files.length);
94
+ const ids = data.files.map((f: { id: string }) => f.id);
95
+ expect(ids).toContain(file1.id);
96
+ expect(ids).toContain(file2.id);
97
+
98
+ // Check if fields are returned
99
+ const f1 = data.files.find((f: { id: string }) => f.id === file1.id) as Record<string, unknown>;
100
+ expect(f1.name).toBeDefined();
101
+ expect(f1.modifiedTime).toBeDefined();
102
+ expect(f1.parents).toBeDefined();
103
+ expect(f1.parents).toContain(parentId);
104
+
105
+ // Strict key check
106
+ const expectedKeys = ['id', 'name', 'mimeType', 'parents', 'modifiedTime', 'size'].sort();
107
+ const actualKeys = Object.keys(f1).sort();
108
+
109
+ // Log for debugging if they don't match (Vitest will show diff)
110
+ if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) {
111
+ console.log('Keys mismatch! Actual:', actualKeys);
112
+ }
113
+ expect(actualKeys).toEqual(expectedKeys);
114
+
115
+ }, 60000);
116
+
117
+ it('should return files with modifiedTime filter', async () => {
118
+ const parentRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
119
+ method: 'POST',
120
+ headers: { ...headers, 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({
122
+ name: 'QueryTestTime_' + randomString(),
123
+ mimeType: 'application/vnd.google-apps.folder'
124
+ })
125
+ });
126
+ const parentId = (await parentRes.json()).id;
127
+
128
+ // Create file 1 (Old)
129
+ const file1 = await fetch(`${config.baseUrl}/drive/v3/files`, {
130
+ method: 'POST',
131
+ headers: { ...headers, 'Content-Type': 'application/json' },
132
+ body: JSON.stringify({ name: 'file1_old', parents: [parentId] })
133
+ }).then(res => res.json());
134
+
135
+ // Wait to ensure distinct modifiedTime
136
+ await new Promise(r => setTimeout(r, 1500));
137
+
138
+ // Checkpoint time
139
+ const checkpointTime = new Date().toISOString();
140
+
141
+ await new Promise(r => setTimeout(r, 1500));
142
+
143
+ // Create file 2 (New)
144
+ const file2 = await fetch(`${config.baseUrl}/drive/v3/files`, {
145
+ method: 'POST',
146
+ headers: { ...headers, 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({ name: 'file2_new', parents: [parentId] })
148
+ }).then(res => res.json());
149
+
150
+ // Query: parent + trashed + modifiedTime >= checkpoint
151
+ const queryParts = [
152
+ `'${parentId}' in parents`,
153
+ `and trashed = false`,
154
+ `and modifiedTime >= '${checkpointTime}'`
155
+ ];
156
+
157
+ const params = new URLSearchParams({
158
+ q: queryParts.join(' '),
159
+ pageSize: '10',
160
+ orderBy: "modifiedTime asc,name asc",
161
+ fields: "files(id,name,mimeType,parents,modifiedTime,size)",
162
+ supportsAllDrives: "true",
163
+ includeItemsFromAllDrives: "true",
164
+ });
165
+
166
+ const url = `${config.baseUrl}/drive/v3/files?${params.toString()}`;
167
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${config.token}` } });
168
+ expect(res.status).toBe(200);
169
+ const data = await res.json();
170
+
171
+ const ids = data.files.map((f: { id: string }) => f.id);
172
+
173
+ // Should contain file2 (New), but NOT file1 (Old)
174
+ expect(ids).toContain(file2.id);
175
+ expect(ids).not.toContain(file1.id);
176
+ }, 60000);
177
+ });
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { getTestConfig, TestConfig } from './config';
3
+
4
+ const randomString = () => Math.random().toString(36).substring(7);
5
+
6
+ describe('Folder Search Parity', () => {
7
+ let config: TestConfig;
8
+ let headers: Record<string, string>;
9
+
10
+ beforeAll(async () => {
11
+ config = await getTestConfig();
12
+ headers = {
13
+ Authorization: `Bearer ${config.token}`
14
+ };
15
+ });
16
+
17
+ it('should return only id for folder search with fields=files(id)', async () => {
18
+ const folderName = 'SearchTest_' + randomString();
19
+ const FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder';
20
+
21
+ // 1. Create a parent folder
22
+ const parentRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
23
+ method: 'POST',
24
+ headers: { ...headers, 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({
26
+ name: 'Parent_' + randomString(),
27
+ mimeType: FOLDER_MIME_TYPE
28
+ })
29
+ });
30
+ expect(parentRes.status).toBe(200);
31
+ const parentId = (await parentRes.json()).id;
32
+
33
+ // 2. Create the target folder inside parent
34
+ const targetRes = await fetch(`${config.baseUrl}/drive/v3/files`, {
35
+ method: 'POST',
36
+ headers: { ...headers, 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({
38
+ name: folderName,
39
+ mimeType: FOLDER_MIME_TYPE,
40
+ parents: [parentId]
41
+ })
42
+ });
43
+ expect(targetRes.status).toBe(200);
44
+ const targetId = (await targetRes.json()).id;
45
+
46
+ // Wait for consistency
47
+ await new Promise(r => setTimeout(r, 2000));
48
+
49
+ // 3. User's Query
50
+ const query = `name = '${folderName}' and '${parentId}' in parents and trashed = false and mimeType = '${FOLDER_MIME_TYPE}'`;
51
+
52
+ const params = new URLSearchParams({
53
+ q: query,
54
+ fields: 'files(id,mimeType)',
55
+ orderBy: 'createdTime asc'
56
+ });
57
+
58
+ const url = `${config.baseUrl}/drive/v3/files?${params.toString()}`;
59
+ console.log('Requesting URL:', url);
60
+
61
+ const res = await fetch(url, { headers });
62
+ expect(res.status).toBe(200);
63
+ const data = await res.json();
64
+
65
+ expect(data.files).toBeDefined();
66
+ expect(data.files.length).toBeGreaterThan(0);
67
+
68
+ const foundFolder = data.files[0];
69
+ expect(foundFolder.id).toBe(targetId);
70
+ expect(foundFolder.mimeType).toBe(FOLDER_MIME_TYPE);
71
+
72
+ // Strict Key Check
73
+ const actualKeys = Object.keys(foundFolder).sort();
74
+ const expectedKeys = ['id', 'mimeType'].sort(); // User requested only id
75
+
76
+ if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) {
77
+ console.log('Keys mismatch! Actual:', actualKeys);
78
+ }
79
+ expect(actualKeys).toEqual(expectedKeys);
80
+ }, 60000);
81
+ });
@@ -212,6 +212,229 @@ describe('Iterate Changes Queries', () => {
212
212
 
213
213
  }, 60000);
214
214
 
215
+ it('should find files where write time >= X, sorted by modifiedTime and name', async () => {
216
+ // 1. Create file OLD (should be excluded)
217
+ const fileOld = await createFileWithContent('file_A_Old', randomString(), config);
218
+
219
+ // Wait to ensure distinct time
220
+ await new Promise(r => setTimeout(r, 1500));
221
+
222
+ // 2. Create ISO-time target files (Equal Time)
223
+ // We create one, get its time, then create another and patch it to match.
224
+ const fileMiddle1 = await createFileWithContent('file_B_Middle1', randomString(), config);
225
+ // Get target time
226
+ const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle1.id}?fields=modifiedTime`, { headers });
227
+ const timeX = (await timeXRes.json()).modifiedTime;
228
+
229
+ // Create second middle file
230
+ const fileMiddle2 = await createFileWithContent('file_B_Middle2', randomString(), config);
231
+
232
+ // Patch fileMiddle2 to match fileMiddle1 time
233
+ await new Promise(r => setTimeout(r, 1100)); // Wait before patching to ensure it would be different otherwise
234
+ const patchBody = JSON.stringify({ modifiedTime: timeX });
235
+ await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
236
+
237
+ // Wait again for New file
238
+ await new Promise(r => setTimeout(r, 1500));
239
+
240
+ // 3. Create file New (should be included, greater match logic)
241
+ const fileNew = await createFileWithContent('file_C_New', randomString(), config);
242
+
243
+ // 4. Query: modifiedTime >= timeX
244
+ // Sort by modifiedTime asc, name asc
245
+ const q = `modifiedTime >= '${timeX}' and trashed = false`;
246
+ const orderBy = 'modifiedTime asc, name asc';
247
+
248
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&fields=files(id,name,modifiedTime)`;
249
+ const res = await fetch(url, { headers });
250
+ if (res.status !== 200) {
251
+ console.error('Error response:', await res.text());
252
+ }
253
+ expect(res.status).toBe(200);
254
+ const data = await res.json();
255
+
256
+ // 5. Verify results
257
+ const resultIds = data.files.map((f: DriveFile) => f.id);
258
+
259
+ // Should NOT contain Old
260
+ expect(resultIds).not.toContain(fileOld.id);
261
+
262
+ // Should contain Middle1, Middle2, New
263
+ const relevantFiles = data.files.filter((f: DriveFile) => [fileMiddle1.id, fileMiddle2.id, fileNew.id].includes(f.id));
264
+ expect(relevantFiles.length).toBe(3);
265
+
266
+ // Verify Order
267
+ // Expect: [Middle1/Middle2 (sorted by NAME), New]
268
+
269
+ // First two should be the middle ones
270
+ const firstTwoIds = [relevantFiles[0].id, relevantFiles[1].id];
271
+ expect(firstTwoIds).toContain(fileMiddle1.id);
272
+ expect(firstTwoIds).toContain(fileMiddle2.id);
273
+
274
+ // Verify secondary sort of first two by NAME
275
+ const expectedFirst = fileMiddle1.name < fileMiddle2.name ? fileMiddle1.id : fileMiddle2.id;
276
+ const expectedSecond = fileMiddle1.name < fileMiddle2.name ? fileMiddle2.id : fileMiddle1.id;
277
+ expect(relevantFiles[0].id).toBe(expectedFirst);
278
+ expect(relevantFiles[1].id).toBe(expectedSecond);
279
+
280
+ // Third should be New
281
+ expect(relevantFiles[2].id).toBe(fileNew.id);
282
+
283
+ // Verify timestamps
284
+ expect(new Date(relevantFiles[0].modifiedTime).toISOString()).toBe(new Date(timeX).toISOString());
285
+ expect(new Date(relevantFiles[1].modifiedTime).toISOString()).toBe(new Date(timeX).toISOString());
286
+ expect(new Date(relevantFiles[2].modifiedTime) > new Date(timeX)).toBe(true);
287
+
288
+ }, 60000);
289
+
290
+ // Test for Name >= Query support
291
+ // VERIFIED: UNSUPPORTED. Returns 500 Internal Error from Google Drive API.
292
+ /*
293
+ it('should find files where name >= X', async () => {
294
+ const fileA = await createFileWithContent('file_A_NameCheck', randomString(), config);
295
+ const fileB = await createFileWithContent('file_B_NameCheck', randomString(), config);
296
+ const fileC = await createFileWithContent('file_C_NameCheck', randomString(), config);
297
+
298
+ // Query: name >= 'file_B_NameCheck'
299
+ // Expect to find B and C, but not A.
300
+ const q = `name >= 'file_B_NameCheck' and trashed = false`;
301
+ const orderBy = 'name asc';
302
+
303
+ console.log('Query:', q);
304
+
305
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}`;
306
+ const res = await fetch(url, { headers });
307
+
308
+ if (res.status !== 200) {
309
+ console.error('Error response:', await res.text());
310
+ }
311
+ // We expect 200 if supported, or 400/500 if not.
312
+ // We will assert 200 to see if it passes.
313
+ expect(res.status).toBe(200);
314
+
315
+ const data = await res.json();
316
+ const names = data.files.map((f: DriveFile) => f.name);
317
+
318
+ expect(names).toContain('file_B_NameCheck');
319
+ expect(names).toContain('file_C_NameCheck');
320
+ expect(names).not.toContain('file_A_NameCheck');
321
+ }, 60000);
322
+ /*
323
+ it('should find files where name >= X', async () => {
324
+ // ... (existing commented code)
325
+ }, 60000);
326
+ */
327
+
328
+ // Test for ID >= Query support
329
+ // VERIFIED: UNSUPPORTED. Returns 400 Bad Request from Google Drive API.
330
+ /*
331
+ it('should find files where id >= X', async () => {
332
+ const fileA = await createFileWithContent('file_A_IdCheck', randomString(), config);
333
+ const fileB = await createFileWithContent('file_B_IdCheck', randomString(), config);
334
+ const fileC = await createFileWithContent('file_C_IdCheck', randomString(), config);
335
+
336
+ // Sort IDs to know order
337
+ const ids = [fileA.id, fileB.id, fileC.id].sort();
338
+ const pivotId = ids[1]; // Middle ID
339
+
340
+ // Query: id >= pivotId
341
+ // Expect to find pivotId and larger ID.
342
+ const q = `id >= '${pivotId}' and trashed = false`;
343
+ const orderBy = 'createdTime asc'; // Sort logic for ID is unsupported, use createdTime or name
344
+
345
+ console.log('Query:', q);
346
+
347
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}`;
348
+ const res = await fetch(url, { headers });
349
+
350
+ if (res.status !== 200) {
351
+ console.error('Error response:', await res.text());
352
+ }
353
+ // We expect 200 if supported, or 400/500 if not.
354
+ expect(res.status).toBe(200);
355
+
356
+ const data = await res.json();
357
+ const resultIds = data.files.map((f: DriveFile) => f.id);
358
+
359
+ expect(resultIds).toContain(ids[1]);
360
+ expect(resultIds).toContain(ids[2]);
361
+ expect(resultIds).not.toContain(ids[0]);
362
+ }, 60000);
363
+ */
364
+
365
+ // MongoDB-style keyset pagination query using 'name'
366
+ // VERIFIED: This query is NOT supported by Google Drive API.
367
+ // 'name > ...' returns 500 Internal Error on Real API.
368
+ // 'id > ...' returns 400 Bad Request on Real API.
369
+ // it('should find files using keyset pagination: (modifiedTime > T) OR (modifiedTime = T AND name > N)', async () => {
370
+ // // 1. Create file OLD (should be excluded)
371
+ // const fileOld = await createFileWithContent('file_A_Old', randomString(), config);
372
+
373
+ // // Wait
374
+ // await new Promise(r => setTimeout(r, 1500));
375
+
376
+ // // 2. Create ISO-time target files (Equal Time)
377
+ // // We need explicit names to test > logic.
378
+ // const name1 = 'file_B_Middle1';
379
+ // const name2 = 'file_B_Middle2';
380
+
381
+ // const fileMiddle1 = await createFileWithContent(name1, randomString(), config);
382
+ // // Get target time
383
+ // const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle1.id}?fields=modifiedTime`, { headers });
384
+ // const timeX = (await timeXRes.json()).modifiedTime;
385
+
386
+ // // Create second middle file
387
+ // const fileMiddle2 = await createFileWithContent(name2, randomString(), config);
388
+
389
+ // // Patch fileMiddle2 to match fileMiddle1 time
390
+ // await new Promise(r => setTimeout(r, 1100));
391
+ // const patchBody = JSON.stringify({ modifiedTime: timeX });
392
+ // await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
393
+
394
+ // // Wait again for New file
395
+ // await new Promise(r => setTimeout(r, 1500));
396
+
397
+ // // 3. Create file New (should be included, greater match logic)
398
+ // const fileNew = await createFileWithContent('file_C_New', randomString(), config);
399
+
400
+ // // Scenario: We want to page after 'file_B_Middle1'.
401
+ // // So we want (modifiedTime > timeX) OR (modifiedTime = timeX AND name > 'file_B_Middle1').
402
+ // // Since names are sorted Middle1 < Middle2 < C_New (alphabetical),
403
+ // // we expect to find Middle2 and C_New.
404
+
405
+ // const cursorName = name1;
406
+ // const cursorTime = timeX;
407
+
408
+
409
+ // // Simplified query to test 'name >' operator support first
410
+ // const q = `name > '${cursorName}' and trashed = false`;
411
+ // const orderBy = 'name asc';
412
+
413
+ // console.log('Query:', q);
414
+
415
+ // console.log('Query:', q);
416
+
417
+ // const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&fields=files(id,name,modifiedTime)`;
418
+ // const res = await fetch(url, { headers });
419
+
420
+ // if (res.status !== 200) {
421
+ // console.error('Error response:', await res.text());
422
+ // }
423
+ // expect(res.status).toBe(200);
424
+ // const data = await res.json();
425
+
426
+ // const resultIds = data.files.map((f: DriveFile) => f.id);
427
+
428
+ // // Must contain Middle2 and fileNew
429
+ // expect(resultIds).toContain(fileMiddle2.id);
430
+ // expect(resultIds).toContain(fileNew.id);
431
+
432
+ // // Must NOT contain Middle1 (it equals cursor, we want >)
433
+ // expect(resultIds).not.toContain(fileMiddle1.id);
434
+ // expect(resultIds).not.toContain(fileOld.id);
435
+
436
+ // }, 60000);
437
+
215
438
  it('should iterate via changes tokens with specific fields', async () => {
216
439
  // User request verification:
217
440
  // const params = new URLSearchParams({
@@ -405,4 +628,293 @@ describe('Iterate Changes Queries', () => {
405
628
  expect(ids.size).toBe(totalFiles);
406
629
 
407
630
  }, 120000); // 25 files creation might take a bit
631
+
632
+ // Test for ID > Query support (v3)
633
+ // VERIFIED: UNSUPPORTED. Returns 400 Bad Request on v3.
634
+ /*
635
+ it('should find files where id > X on v3 API', async () => {
636
+ const fileA = await createFileWithContent('file_A_IdCheck_v3', randomString(), config);
637
+ const fileB = await createFileWithContent('file_B_IdCheck_v3', randomString(), config);
638
+ const fileC = await createFileWithContent('file_C_IdCheck_v3', randomString(), config);
639
+
640
+ // Sort IDs to know order
641
+ const ids = [fileA.id, fileB.id, fileC.id].sort();
642
+ const pivotId = ids[1]; // Middle ID
643
+
644
+ // Query: id > pivotId
645
+ const q = `id > '${pivotId}' and trashed = false`;
646
+ const orderBy = 'createdTime asc';
647
+
648
+ console.log('Query v3:', q);
649
+
650
+ const url = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}`;
651
+ const res = await fetch(url, { headers });
652
+
653
+ if (res.status !== 200) {
654
+ console.error('Error response v3:', await res.text());
655
+ }
656
+ expect(res.status).toBe(200);
657
+
658
+ const data = await res.json();
659
+ const resultIds = data.files.map((f: DriveFile) => f.id);
660
+
661
+ expect(resultIds).toContain(ids[2]);
662
+ expect(resultIds).not.toContain(ids[1]); // strict >
663
+ expect(resultIds).not.toContain(ids[0]);
664
+ }, 60000);
665
+ */
666
+
667
+ // Test for ID > Query support on v2 API
668
+ // VERIFIED: UNSUPPORTED. Returns 400 Bad Request on v2.
669
+ /*
670
+ it('should find files where id > X on v2 API', async () => {
671
+ // ... (existing code)
672
+ }, 60000);
673
+ */
674
+
675
+ // MongoDB-style keyset pagination query using 'title' on v2 API
676
+ // VERIFIED: UNSUPPORTED. Returns 500 Internal Error on v2 for this complex query.
677
+ /*
678
+ it('should find files using keyset pagination on v2: (modifiedDate > T) OR (modifiedDate = T AND title > N)', async () => {
679
+ // 1. Create file OLD (should be excluded)
680
+ const fileOld = await createFileWithContent('file_A_Old_v2', randomString(), config);
681
+
682
+ // Wait
683
+ await new Promise(r => setTimeout(r, 1500));
684
+
685
+ // 2. Create ISO-time target files (Equal Time)
686
+ // We need explicit names to test > logic.
687
+ const name1 = 'file_B_Middle1_v2';
688
+ const name2 = 'file_B_Middle2_v2';
689
+
690
+ const fileMiddle1 = await createFileWithContent(name1, randomString(), config);
691
+ // Get target time (using v3 to get precise time, assuming v2 sees same time)
692
+ const timeXRes = await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle1.id}?fields=modifiedTime`, { headers });
693
+ const timeX = (await timeXRes.json()).modifiedTime;
694
+
695
+ // Create second middle file
696
+ const fileMiddle2 = await createFileWithContent(name2, randomString(), config);
697
+
698
+ // Patch fileMiddle2 to match fileMiddle1 time
699
+ await new Promise(r => setTimeout(r, 1100));
700
+ const patchBody = JSON.stringify({ modifiedTime: timeX });
701
+ await fetch(`${config.baseUrl}/drive/v3/files/${fileMiddle2.id}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' }, body: patchBody });
702
+
703
+ // Wait again for New file
704
+ await new Promise(r => setTimeout(r, 1500));
705
+
706
+ // 3. Create file New (should be included, greater match logic)
707
+ const fileNew = await createFileWithContent('file_C_New_v2', randomString(), config);
708
+
709
+ // Scenario: We want to page after 'file_B_Middle1'.
710
+ // So we want (modifiedDate > timeX) OR (modifiedDate = timeX AND title > 'file_B_Middle1_v2').
711
+
712
+ const cursorTitle = name1;
713
+ const cursorTime = timeX;
714
+
715
+ // Query using v2 fields: modifiedDate and title
716
+ const q = `(modifiedDate > '${cursorTime}') or (modifiedDate = '${cursorTime}' and title > '${cursorTitle}') and trashed = false`;
717
+ // v2 uses 'title' for name
718
+
719
+ console.log('Query v2 Keyset:', q);
720
+
721
+ const url = `${config.baseUrl}/drive/v2/files?q=${encodeURIComponent(q)}`;
722
+ const res = await fetch(url, { headers });
723
+
724
+ if (res.status !== 200) {
725
+ console.error('Error response v2 Keyset:', await res.text());
726
+ }
727
+ expect(res.status).toBe(200);
728
+ const data = await res.json();
729
+
730
+ const resultIds = data.items.map((f: any) => f.id);
731
+
732
+ // Must contain Middle2 and fileNew
733
+ expect(resultIds).toContain(fileMiddle2.id);
734
+ expect(resultIds).toContain(fileNew.id);
735
+
736
+ // Must NOT contain Middle1 (it equals cursor, we want >)
737
+ expect(resultIds).not.toContain(fileMiddle1.id);
738
+ expect(resultIds).not.toContain(fileOld.id);
739
+ }, 60000);
740
+ */
741
+
742
+ // Test for Paginated Changes in Parent Folder (v3)
743
+ it('should paginate through "changes" (files > time) in a specific parent folder (v3)', async () => {
744
+ // 1. Create Parent Folder
745
+ const resP = await fetch(`${config.baseUrl}/drive/v3/files`, {
746
+ method: 'POST',
747
+ headers: { ...headers, 'Content-Type': 'application/json' },
748
+ body: JSON.stringify({
749
+ name: 'ParentFolder_Pagination_v3_' + randomString(),
750
+ mimeType: 'application/vnd.google-apps.folder'
751
+ })
752
+ });
753
+ expect(resP.status).toBe(200);
754
+ const parentId = (await resP.json()).id;
755
+
756
+ // 2. Create files
757
+ const totalFiles = 6;
758
+ const baseName = 'InFolder_v3_' + randomString();
759
+
760
+ // Create files
761
+ for (let i = 0; i < totalFiles; i++) {
762
+ // Use body for parents to be safe
763
+ const res = await fetch(`${config.baseUrl}/drive/v3/files?fields=id,parents`, {
764
+ method: 'POST',
765
+ headers: { ...headers, 'Content-Type': 'application/json' },
766
+ body: JSON.stringify({
767
+ name: `${baseName}_${i}`,
768
+ mimeType: 'text/plain',
769
+ parents: [parentId]
770
+ })
771
+ });
772
+ if (res.status !== 200) {
773
+ console.error('Create file failed:', await res.text());
774
+ }
775
+ expect(res.status).toBe(200);
776
+ const created = await res.json();
777
+ if (!created.parents || !created.parents.includes(parentId)) {
778
+ console.error('File created but parent missing:', created);
779
+ }
780
+ }
781
+
782
+ // Wait for potential eventual consistency/indexing
783
+ console.log('Waiting 5s for indexing...');
784
+ await new Promise(r => setTimeout(r, 5000));
785
+
786
+ // Get one file to check time
787
+ const listOne = await fetch(`${config.baseUrl}/drive/v3/files?q='${parentId}' in parents&pageSize=1&fields=files(modifiedTime)`, { headers });
788
+ const oneData = await listOne.json();
789
+ if (!oneData.files || oneData.files.length === 0) {
790
+ throw new Error('No files found in parent check!');
791
+ }
792
+ const refTime = oneData.files[0].modifiedTime;
793
+ console.log('Reference File Time:', refTime);
794
+
795
+ // Filter: modifiedTime > refTime - 1 hour (to safely include all)
796
+ const startTime = new Date(new Date(refTime).getTime() - 3600000).toISOString();
797
+ console.log('Filter Start Time:', startTime);
798
+
799
+ // 3. Query with Pagination
800
+ const q = `'${parentId}' in parents and modifiedTime > '${startTime}' and trashed = false`;
801
+ const orderBy = 'modifiedTime asc';
802
+ const pageSize = 2; // Force pagination
803
+
804
+ const collectedFiles: DriveFile[] = [];
805
+ let pageToken: string | undefined;
806
+
807
+ console.log('Query v3 Parent Pagination:', q);
808
+
809
+ do {
810
+ const url: string = `${config.baseUrl}/drive/v3/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&pageSize=${pageSize}` + (pageToken ? `&pageToken=${pageToken}` : '');
811
+ const res = await fetch(url, { headers });
812
+
813
+ if (res.status !== 200) {
814
+ console.error('Pagination Error v3:', await res.text());
815
+ }
816
+ expect(res.status).toBe(200);
817
+ const data = await res.json();
818
+
819
+ if (data.files) {
820
+ console.log(`Page returned ${data.files.length} files. NextToken: ${!!data.nextPageToken}`);
821
+ collectedFiles.push(...data.files);
822
+ }
823
+ pageToken = data.nextPageToken;
824
+
825
+ if (collectedFiles.length > totalFiles + 5) break;
826
+ } while (pageToken);
827
+
828
+ expect(collectedFiles.length).toBe(totalFiles);
829
+ const names = collectedFiles.map(f => f.name);
830
+ expect(names).toHaveLength(totalFiles);
831
+ }, 120000);
832
+
833
+ // Test for Paginated Changes in Parent Folder (v2)
834
+ it('should paginate through "changes" (files > time) in a specific parent folder (v2)', async () => {
835
+ // 1. Create Parent Folder (v3 create is fine, reusable for v2 test)
836
+ const resP = await fetch(`${config.baseUrl}/drive/v3/files`, {
837
+ method: 'POST',
838
+ headers: { ...headers, 'Content-Type': 'application/json' },
839
+ body: JSON.stringify({
840
+ name: 'ParentFolder_Pagination_v2_' + randomString(),
841
+ mimeType: 'application/vnd.google-apps.folder'
842
+ })
843
+ });
844
+ expect(resP.status).toBe(200);
845
+ const parentId = (await resP.json()).id;
846
+
847
+ // 2. Create files (v3 create is fine)
848
+ const totalFiles = 6;
849
+ const baseName = 'InFolder_v2_' + randomString();
850
+
851
+ for (let i = 0; i < totalFiles; i++) {
852
+ const res = await fetch(`${config.baseUrl}/drive/v3/files?fields=id,parents`, {
853
+ method: 'POST',
854
+ headers: { ...headers, 'Content-Type': 'application/json' },
855
+ body: JSON.stringify({
856
+ name: `${baseName}_${i}`,
857
+ mimeType: 'text/plain',
858
+ parents: [parentId]
859
+ })
860
+ });
861
+ expect(res.status).toBe(200);
862
+ }
863
+
864
+ console.log('Waiting 5s for indexing (v2)...');
865
+ await new Promise(r => setTimeout(r, 5000));
866
+
867
+ // Get one file to check time (v2 uses modifiedDate)
868
+ // v2 list: items
869
+ const listOne = await fetch(`${config.baseUrl}/drive/v2/files?q='${parentId}' in parents&maxResults=1`, { headers });
870
+ if (listOne.status !== 200) {
871
+ console.error('v2 check failed:', await listOne.text());
872
+ }
873
+ const oneData = await listOne.json();
874
+
875
+ if (!oneData.items || oneData.items.length === 0) {
876
+ throw new Error('No files found in parent check v2!');
877
+ }
878
+ const refTime = oneData.items[0].modifiedDate;
879
+ console.log('Reference File Time v2:', refTime);
880
+
881
+ // Filter: modifiedDate > refTime - 1 hour
882
+ const startTime = new Date(new Date(refTime).getTime() - 3600000).toISOString();
883
+ console.log('Filter Start Time v2:', startTime);
884
+
885
+ // 3. Query with Pagination (v2 syntax)
886
+ const q = `'${parentId}' in parents and modifiedDate > '${startTime}' and trashed = false`;
887
+ const orderBy = 'modifiedDate asc';
888
+ const pageSize = 2; // maxResults
889
+
890
+ // v2 uses 'items' and 'title', not 'name'
891
+ interface DriveV2File { id: string; title: string; modifiedDate: string; }
892
+ const collectedFiles: DriveV2File[] = [];
893
+ let pageToken: string | undefined;
894
+
895
+ console.log('Query v2 Parent Pagination:', q);
896
+
897
+ do {
898
+ const url: string = `${config.baseUrl}/drive/v2/files?q=${encodeURIComponent(q)}&orderBy=${encodeURIComponent(orderBy)}&maxResults=${pageSize}` + (pageToken ? `&pageToken=${pageToken}` : '');
899
+ const res = await fetch(url, { headers });
900
+
901
+ if (res.status !== 200) {
902
+ console.error('Pagination Error v2:', await res.text());
903
+ }
904
+ expect(res.status).toBe(200);
905
+ const data = await res.json();
906
+
907
+ if (data.items) {
908
+ console.log(`Page returned ${data.items.length} items. NextToken: ${!!data.nextPageToken}`);
909
+ collectedFiles.push(...data.items);
910
+ }
911
+ pageToken = data.nextPageToken;
912
+
913
+ if (collectedFiles.length > totalFiles + 5) break;
914
+ } while (pageToken);
915
+
916
+ expect(collectedFiles.length).toBe(totalFiles);
917
+ const names = collectedFiles.map((f) => f.title); // v2 uses title
918
+ expect(names).toHaveLength(totalFiles);
919
+ }, 120000);
408
920
  });