Skip to content

Commit 111ddca

Browse files
committed
fix: System, unit, and conformance test cases
1 parent d4ce4b4 commit 111ddca

6 files changed

Lines changed: 351 additions & 151 deletions

File tree

handwritten/storage/src/storage-transport.ts

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ export class StorageTransport {
133133
reqOpts.projectId || (await this.authClient.getProjectId());
134134
}
135135

136+
if (reqOpts.queryParameters && this.projectId) {
137+
reqOpts.queryParameters.project = this.projectId;
138+
}
139+
136140
// Header Construction
137141
const headers = this.#prepareHeaders(reqOpts);
138142

@@ -145,7 +149,12 @@ export class StorageTransport {
145149
}
146150

147151
const urlString = reqOpts.url?.toString() || '';
148-
const isAbsolute = urlString.startsWith('http');
152+
const isAbsolute = this.#isValidUrl(urlString);
153+
154+
// Determine the base URL for the request
155+
const requestUrl = isAbsolute
156+
? urlString
157+
: new URL(urlString, this.baseUrl).toString();
149158

150159
try {
151160
const requestPromise = this.authClient.request<T>({
@@ -159,20 +168,22 @@ export class StorageTransport {
159168
},
160169
...reqOpts,
161170
data: reqOpts.body,
162-
params: isAbsolute ? undefined : reqOpts.queryParameters,
171+
params: reqOpts.queryParameters,
172+
paramsSerializer: this.#paramsSerializer,
163173
headers,
164-
url: isAbsolute
165-
? urlString
166-
: this.#buildUrl(reqOpts.url?.toString(), reqOpts.queryParameters),
174+
url: requestUrl,
167175
timeout: this.timeout,
168176
// eslint-disable-next-line @typescript-eslint/no-explicit-any
169177
...({decompress: false} as any),
170178
validateStatus: status => {
171-
if (urlString.includes('uploadType=resumable')) {
172-
return (status >= 200 && status < 300) || status === 308;
173-
}
174-
return status >= 200 && status < 300;
175-
}, //status => status >= 200 && status < 300,
179+
const isResumable =
180+
reqOpts.queryParameters?.uploadType === 'resumable' ||
181+
reqOpts.url?.toString().includes('uploadType=resumable');
182+
183+
return (
184+
(status >= 200 && status < 300) || (isResumable && status === 308)
185+
);
186+
},
176187
});
177188

178189
// Response Handling
@@ -218,25 +229,6 @@ export class StorageTransport {
218229
return finalHeaders;
219230
}
220231

221-
#buildUrl(pathUri = '', queryParameters: StorageQueryParameters = {}): URL {
222-
// Sync project ID in params if necessary
223-
if (
224-
'project' in queryParameters &&
225-
queryParameters.project !== this.projectId
226-
) {
227-
queryParameters.project = this.projectId;
228-
}
229-
230-
let url: URL;
231-
if (this.#isValidUrl(pathUri)) {
232-
url = new URL(pathUri);
233-
} else {
234-
url = new URL(pathUri, this.baseUrl); // Safer construction
235-
}
236-
url.search = this.#buildRequestQueryParams(queryParameters);
237-
return url;
238-
}
239-
240232
#isValidUrl(url: string): boolean {
241233
try {
242234
return Boolean(new URL(url));
@@ -245,6 +237,26 @@ export class StorageTransport {
245237
}
246238
}
247239

240+
/**
241+
* Serializes query parameters into a string.
242+
* Specifically handles arrays by appending each value individually
243+
* to satisfy GCS "repeated key" requirements (e.g., for IAM permissions).
244+
*/
245+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
246+
#paramsSerializer = (params: Record<string, any>): string => {
247+
const searchParams = new URLSearchParams();
248+
for (const [key, value] of Object.entries(params)) {
249+
if (value === undefined) continue;
250+
251+
if (Array.isArray(value)) {
252+
value.forEach(v => searchParams.append(key, String(v)));
253+
} else {
254+
searchParams.set(key, String(value));
255+
}
256+
}
257+
return searchParams.toString();
258+
};
259+
248260
#buildRequestHeaders(requestHeaders = {}) {
249261
const headers = new Headers(requestHeaders);
250262
headers.set('User-Agent', this.#getUserAgentString());
@@ -255,21 +267,6 @@ export class StorageTransport {
255267
return headers;
256268
}
257269

258-
#buildRequestQueryParams(queryParameters: StorageQueryParameters): string {
259-
const qp = new URLSearchParams();
260-
for (const [key, value] of Object.entries(queryParameters)) {
261-
if (value === undefined) continue;
262-
263-
if (Array.isArray(value)) {
264-
// This is the fix: append each item individually for repeated keys
265-
value.forEach(item => qp.append(key, String(item)));
266-
} else {
267-
qp.set(key, String(value));
268-
}
269-
}
270-
return qp.toString();
271-
}
272-
273270
#getUserAgentString(): string {
274271
const base = getUserAgentString();
275272
return this.providedUserAgent ? `${this.providedUserAgent} ${base}` : base;

handwritten/storage/system-test/fixtures/index-cjs.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// eslint-disable-next-line no-undef
15+
/* eslint-disable node/no-missing-require, no-unused-vars, no-undef */
1616
const {Storage} = require('@google-cloud/storage');
1717

1818
function main() {
19-
// eslint-disable-next-line no-unused-vars
2019
const storage = new Storage();
2120
}
2221

handwritten/storage/system-test/fixtures/index-esm.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// eslint-disable-next-line no-undef
16-
const {Storage} = require('@google-cloud/storage');
15+
/* eslint-disable node/no-missing-import, no-unused-vars */
16+
import {Storage} from '@google-cloud/storage';
1717

1818
function main() {
19-
// eslint-disable-next-line no-unused-vars
2019
const storage = new Storage();
2120
}
2221

handwritten/storage/system-test/kitchen.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ describe('resumable-upload', () => {
5454
retryableErrorFn: RETRYABLE_ERR_FN_DEFAULT,
5555
};
5656

57-
const bucket = new Storage({retryOptions}).bucket(bucketName);
57+
const bucket = new Storage({
58+
projectId: process.env.PROJECT_ID,
59+
retryOptions: retryOptions,
60+
}).bucket(bucketName);
5861
let filePath: string;
5962

6063
before(async () => {
@@ -96,7 +99,7 @@ describe('resumable-upload', () => {
9699
// see: https://cloud.google.com/storage/docs/exponential-backoff:
97100
const ms = Math.pow(2, retries) * 1000 + Math.random() * 2000;
98101
console.info(`retrying "${title}" in ${ms}ms`);
99-
setTimeout(done(), ms);
102+
setTimeout(() => done, ms);
100103
}
101104

102105
it('should work', done => {

0 commit comments

Comments
 (0)