Skip to content

Commit e99a2a2

Browse files
committed
add some impersonate query unit tests
1 parent 7b7e617 commit e99a2a2

File tree

3 files changed

+153
-10
lines changed

3 files changed

+153
-10
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,24 @@ export class DataConnectApiClient {
169169
});
170170
}
171171

172+
/**
173+
* Executes a GraphQL query with impersonation.
174+
*
175+
* @param options - The GraphQL options, including impersonation details.
176+
* @returns A promise that fulfills with the GraphQL response.
177+
*/
172178
public async impersonateQuery<GraphqlResponse, Variables>(
173179
options: GraphqlOptions<Variables>
174180
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
175181
return this.impersonateHelper(IMPERSONATE_QUERY_ENDPOINT, options);
176182
}
177183

184+
/**
185+
* Executes a GraphQL mutation with impersonation.
186+
*
187+
* @param options - The GraphQL options, including impersonation details.
188+
* @returns A promise that fulfills with the GraphQL response.
189+
*/
178190
public async impersonateMutation<GraphqlResponse, Variables>(
179191
options: GraphqlOptions<Variables>
180192
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {

src/data-connect/data-connect.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,48 @@ export class DataConnect {
9292
}
9393

9494
/**
95-
* Execute an arbitrary read-only GraphQL query
96-
*
97-
* @param query - The GraphQL read-only query.
98-
* @param options - Optional {@link GraphqlOptions} when executing a read-only GraphQL query.
99-
*
100-
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
101-
*/
95+
* Execute an arbitrary read-only GraphQL query
96+
*
97+
* @param query - The GraphQL read-only query.
98+
* @param options - Optional {@link GraphqlOptions} when executing a read-only GraphQL query.
99+
*
100+
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
101+
*/
102102
public executeGraphqlRead<GraphqlResponse, Variables>(
103103
query: string,
104104
options?: GraphqlOptions<Variables>,
105105
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
106106
return this.client.executeGraphqlRead(query, options);
107107
}
108108

109+
/**
110+
* Executes a pre-defined GraphQL query with impersonation
111+
*
112+
* The query must be defined in your Data Connect GQL files.
113+
*
114+
* @param options - The GraphQL options, must include impersonation details.
115+
* @returns A promise that fulfills with the GraphQL response.
116+
*/
117+
public async impersonateQuery<GraphqlResponse, Variables>(
118+
options: GraphqlOptions<Variables>
119+
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
120+
return this.client.impersonateQuery(options);
121+
}
122+
123+
/**
124+
* Executes a pre-defined GraphQL mutation with impersonation.
125+
*
126+
* The mutation must be defined in your Data Connect GQL files.
127+
*
128+
* @param options - The GraphQL options, must include impersonation details.
129+
* @returns A promise that fulfills with the GraphQL response.
130+
*/
131+
public async impersonateMutation<GraphqlResponse, Variables>(
132+
options: GraphqlOptions<Variables>
133+
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
134+
return this.client.impersonateMutation(options);
135+
}
136+
109137
/**
110138
* Insert a single row into the specified table.
111139
*

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

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ import * as mocks from '../../resources/mocks';
2727
import { DATA_CONNECT_ERROR_CODE_MAPPING, DataConnectApiClient, FirebaseDataConnectError }
2828
from '../../../src/data-connect/data-connect-api-client-internal';
2929
import { FirebaseApp } from '../../../src/app/firebase-app';
30-
import { ConnectorConfig } from '../../../src/data-connect';
30+
import { ConnectorConfig, GraphqlOptions } from '../../../src/data-connect';
3131
import { getMetricsHeader, getSdkVersion } from '../../../src/utils';
32+
import { DataConnectService } from '../../../src/data-connect/data-connect';
3233

3334
describe('DataConnectApiClient', () => {
3435

@@ -231,11 +232,113 @@ describe('DataConnectApiClient', () => {
231232
});
232233
});
233234

234-
describe('impersonate', () => {
235+
describe('impersonateQuery', () => {
236+
const unauthenticatedOptions: GraphqlOptions<unknown> =
237+
{ operationName: 'operationName', impersonate: { unauthenticated: true } };
238+
239+
it('should reject when no operationName is provided', () => {
240+
apiClient.impersonateQuery({ impersonate: { unauthenticated: true } })
241+
.should.eventually.be.rejectedWith('`query` must be a non-empty string.');
242+
apiClient.impersonateQuery({ operationName: undefined, impersonate: { unauthenticated: true } })
243+
.should.eventually.be.rejectedWith('`query` must be a non-empty string.');
244+
});
245+
it('should reject when no impersonate object is provided', () => {
246+
apiClient.impersonateQuery({ operationName: 'queryName' })
247+
.should.eventually.be.rejectedWith('GraphqlOptions must be a non-null object');
248+
apiClient.impersonateQuery({ operationName: 'queryName', impersonate: undefined })
249+
.should.eventually.be.rejectedWith('GraphqlOptions must be a non-null object');
250+
});
235251
it('should reject when project id is not available', () => {
236-
return clientWithoutProjectId.executeGraphql('query', {})
252+
clientWithoutProjectId.impersonateQuery(unauthenticatedOptions)
237253
.should.eventually.be.rejectedWith(noProjectId);
238254
});
255+
256+
it('should reject when a full platform error response is received', () => {
257+
sandbox
258+
.stub(HttpClient.prototype, 'send')
259+
.rejects(utils.errorFrom(ERROR_RESPONSE, 404));
260+
const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found');
261+
return apiClient.impersonateQuery(unauthenticatedOptions)
262+
.should.eventually.be.rejected.and.deep.include(expected);
263+
});
264+
265+
it('should reject with unknown-error when error code is not present', () => {
266+
sandbox
267+
.stub(HttpClient.prototype, 'send')
268+
.rejects(utils.errorFrom({}, 404));
269+
const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}');
270+
return apiClient.impersonateQuery(unauthenticatedOptions)
271+
.should.eventually.be.rejected.and.deep.include(expected);
272+
});
273+
274+
it('should reject with unknown-error for non-json response', () => {
275+
sandbox
276+
.stub(HttpClient.prototype, 'send')
277+
.rejects(utils.errorFrom('not json', 404));
278+
const expected = new FirebaseDataConnectError(
279+
'unknown-error', 'Unexpected response with status: 404 and body: not json');
280+
return apiClient.impersonateQuery(unauthenticatedOptions)
281+
.should.eventually.be.rejected.and.deep.include(expected);
282+
});
283+
284+
it('should reject when rejected with a FirebaseDataConnectError', () => {
285+
const expected = new FirebaseDataConnectError('internal-error', 'socket hang up');
286+
sandbox
287+
.stub(HttpClient.prototype, 'send')
288+
.rejects(expected);
289+
return apiClient.impersonateQuery(unauthenticatedOptions)
290+
.should.eventually.be.rejected.and.deep.include(expected);
291+
});
292+
293+
it('should resolve with the GraphQL response on success', () => {
294+
interface UsersResponse {
295+
users: [
296+
user: {
297+
id: string;
298+
name: string;
299+
address: string;
300+
}
301+
];
302+
}
303+
const stub = sandbox
304+
.stub(HttpClient.prototype, 'send')
305+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
306+
return apiClient.impersonateQuery<UsersResponse, unknown>(unauthenticatedOptions)
307+
.then((resp) => {
308+
expect(resp.data.users).to.be.not.empty;
309+
expect(resp.data.users[0].name).to.be.not.undefined;
310+
expect(resp.data.users[0].address).to.be.not.undefined;
311+
expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users);
312+
expect(stub).to.have.been.calledOnce.and.calledWith({
313+
method: 'POST',
314+
url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}/connectors/${DataConnectService.getId(connectorConfig)}:impersonateQuery`,
315+
headers: EXPECTED_HEADERS,
316+
data: {
317+
operationName: unauthenticatedOptions.operationName,
318+
extensions: { impersonate: unauthenticatedOptions.impersonate }
319+
}
320+
});
321+
});
322+
});
323+
324+
it('should use DATA_CONNECT_EMULATOR_HOST if set', () => {
325+
process.env.DATA_CONNECT_EMULATOR_HOST = 'localhost:9399';
326+
const stub = sandbox
327+
.stub(HttpClient.prototype, 'send')
328+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
329+
return apiClient.impersonateQuery(unauthenticatedOptions)
330+
.then(() => {
331+
expect(stub).to.have.been.calledOnce.and.calledWith({
332+
method: 'POST',
333+
url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}/connectors/${DataConnectService.getId(connectorConfig)}:impersonateQuery`,
334+
headers: EMULATOR_EXPECTED_HEADERS,
335+
data: {
336+
operationName: unauthenticatedOptions.operationName,
337+
extensions: { impersonate: unauthenticatedOptions.impersonate }
338+
}
339+
});
340+
});
341+
});
239342
});
240343
});
241344

0 commit comments

Comments
 (0)