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.
- package/.agent/rules/project-guidelines.md +5 -0
- package/.github/workflows/release.yml +2 -2
- package/dist/index.js +8 -0
- package/dist/mappers.d.ts +8 -0
- package/dist/mappers.js +79 -0
- package/dist/routes/v3.js +15 -3
- package/package.json +1 -1
- package/src/index.ts +9 -0
- package/src/mappers.ts +84 -0
- package/src/routes/v3.ts +14 -2
- package/test/folder_query.test.ts +177 -0
- package/test/folder_search.test.ts +81 -0
- package/test/iterate_changes.test.ts +512 -0
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
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
|
-
|
|
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
|
});
|