Skip to content

Commit c76eb46

Browse files
committed
add non-authentication impersonate unit tests
1 parent e99a2a2 commit c76eb46

File tree

2 files changed

+135
-26
lines changed

2 files changed

+135
-26
lines changed

src/data-connect/data-connect-api-client-internal.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -143,32 +143,6 @@ export class DataConnectApiClient {
143143
});
144144
}
145145

146-
/**
147-
* Makes a GraphQL request to the specified url.
148-
*
149-
* @param url - The URL to send the request to.
150-
* @param data - The GraphQL request payload.
151-
* @returns A promise that fulfills with the GraphQL response, or throws an error.
152-
*/
153-
private async makeGqlRequest<GraphqlResponse>(url: string, data: object):
154-
Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
155-
const request: HttpRequestConfig = {
156-
method: 'POST',
157-
url,
158-
headers: DATA_CONNECT_CONFIG_HEADERS,
159-
data,
160-
};
161-
const resp = await this.httpClient.send(request);
162-
if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) {
163-
const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' ');
164-
throw new FirebaseDataConnectError(
165-
DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, allMessages);
166-
}
167-
return Promise.resolve({
168-
data: resp.data.data as GraphqlResponse,
169-
});
170-
}
171-
172146
/**
173147
* Executes a GraphQL query with impersonation.
174148
*
@@ -310,6 +284,32 @@ export class DataConnectApiClient {
310284
});
311285
}
312286

287+
/**
288+
* Makes a GraphQL request to the specified url.
289+
*
290+
* @param url - The URL to send the request to.
291+
* @param data - The GraphQL request payload.
292+
* @returns A promise that fulfills with the GraphQL response, or throws an error.
293+
*/
294+
private async makeGqlRequest<GraphqlResponse>(url: string, data: object):
295+
Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
296+
const request: HttpRequestConfig = {
297+
method: 'POST',
298+
url,
299+
headers: DATA_CONNECT_CONFIG_HEADERS,
300+
data,
301+
};
302+
const resp = await this.httpClient.send(request);
303+
if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) {
304+
const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' ');
305+
throw new FirebaseDataConnectError(
306+
DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, allMessages);
307+
}
308+
return Promise.resolve({
309+
data: resp.data.data as GraphqlResponse,
310+
});
311+
}
312+
313313
private toFirebaseError(err: RequestResponseError): PrefixedFirebaseError {
314314
if (err instanceof PrefixedFirebaseError) {
315315
return err;

test/unit/data-connect/data-connect-api-client-internal.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,115 @@ describe('DataConnectApiClient', () => {
340340
});
341341
});
342342
});
343+
344+
describe('impersonateMutation', () => {
345+
const unauthenticatedOptions: GraphqlOptions<unknown> =
346+
{ operationName: 'operationName', impersonate: { unauthenticated: true } };
347+
348+
it('should reject when no operationName is provided', () => {
349+
apiClient.impersonateMutation({ impersonate: { unauthenticated: true } })
350+
.should.eventually.be.rejectedWith('`query` must be a non-empty string.');
351+
apiClient.impersonateMutation({ operationName: undefined, impersonate: { unauthenticated: true } })
352+
.should.eventually.be.rejectedWith('`query` must be a non-empty string.');
353+
});
354+
it('should reject when no impersonate object is provided', () => {
355+
apiClient.impersonateMutation({ operationName: 'queryName' })
356+
.should.eventually.be.rejectedWith('GraphqlOptions must be a non-null object');
357+
apiClient.impersonateMutation({ operationName: 'queryName', impersonate: undefined })
358+
.should.eventually.be.rejectedWith('GraphqlOptions must be a non-null object');
359+
});
360+
it('should reject when project id is not available', () => {
361+
clientWithoutProjectId.impersonateMutation(unauthenticatedOptions)
362+
.should.eventually.be.rejectedWith(noProjectId);
363+
});
364+
365+
it('should reject when a full platform error response is received', () => {
366+
sandbox
367+
.stub(HttpClient.prototype, 'send')
368+
.rejects(utils.errorFrom(ERROR_RESPONSE, 404));
369+
const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found');
370+
return apiClient.impersonateMutation(unauthenticatedOptions)
371+
.should.eventually.be.rejected.and.deep.include(expected);
372+
});
373+
374+
it('should reject with unknown-error when error code is not present', () => {
375+
sandbox
376+
.stub(HttpClient.prototype, 'send')
377+
.rejects(utils.errorFrom({}, 404));
378+
const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}');
379+
return apiClient.impersonateMutation(unauthenticatedOptions)
380+
.should.eventually.be.rejected.and.deep.include(expected);
381+
});
382+
383+
it('should reject with unknown-error for non-json response', () => {
384+
sandbox
385+
.stub(HttpClient.prototype, 'send')
386+
.rejects(utils.errorFrom('not json', 404));
387+
const expected = new FirebaseDataConnectError(
388+
'unknown-error', 'Unexpected response with status: 404 and body: not json');
389+
return apiClient.impersonateMutation(unauthenticatedOptions)
390+
.should.eventually.be.rejected.and.deep.include(expected);
391+
});
392+
393+
it('should reject when rejected with a FirebaseDataConnectError', () => {
394+
const expected = new FirebaseDataConnectError('internal-error', 'socket hang up');
395+
sandbox
396+
.stub(HttpClient.prototype, 'send')
397+
.rejects(expected);
398+
return apiClient.impersonateMutation(unauthenticatedOptions)
399+
.should.eventually.be.rejected.and.deep.include(expected);
400+
});
401+
402+
it('should resolve with the GraphQL response on success', () => {
403+
interface UsersResponse {
404+
users: [
405+
user: {
406+
id: string;
407+
name: string;
408+
address: string;
409+
}
410+
];
411+
}
412+
const stub = sandbox
413+
.stub(HttpClient.prototype, 'send')
414+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
415+
return apiClient.impersonateMutation<UsersResponse, unknown>(unauthenticatedOptions)
416+
.then((resp) => {
417+
expect(resp.data.users).to.be.not.empty;
418+
expect(resp.data.users[0].name).to.be.not.undefined;
419+
expect(resp.data.users[0].address).to.be.not.undefined;
420+
expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users);
421+
expect(stub).to.have.been.calledOnce.and.calledWith({
422+
method: 'POST',
423+
url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}/connectors/${DataConnectService.getId(connectorConfig)}:impersonateMutation`,
424+
headers: EXPECTED_HEADERS,
425+
data: {
426+
operationName: unauthenticatedOptions.operationName,
427+
extensions: { impersonate: unauthenticatedOptions.impersonate }
428+
}
429+
});
430+
});
431+
});
432+
433+
it('should use DATA_CONNECT_EMULATOR_HOST if set', () => {
434+
process.env.DATA_CONNECT_EMULATOR_HOST = 'localhost:9399';
435+
const stub = sandbox
436+
.stub(HttpClient.prototype, 'send')
437+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
438+
return apiClient.impersonateMutation(unauthenticatedOptions)
439+
.then(() => {
440+
expect(stub).to.have.been.calledOnce.and.calledWith({
441+
method: 'POST',
442+
url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}/connectors/${DataConnectService.getId(connectorConfig)}:impersonateMutation`,
443+
headers: EMULATOR_EXPECTED_HEADERS,
444+
data: {
445+
operationName: unauthenticatedOptions.operationName,
446+
extensions: { impersonate: unauthenticatedOptions.impersonate }
447+
}
448+
});
449+
});
450+
});
451+
});
343452
});
344453

345454
describe('DataConnectApiClient CRUD helpers', () => {

0 commit comments

Comments
 (0)