Skip to content

Commit fe78b87

Browse files
author
FOLUSO ONATEMOWO
committed
Modified the geturl function to account for a connector being passed into the connectorConfig so that executeGraphql and executeGraphqlRead don't try to retrieve data from wrong endpoints. Included unit tests for the executeMutation function.
1 parent 3f45285 commit fe78b87

File tree

4 files changed

+195
-10
lines changed

4 files changed

+195
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ firebase-admin-*.tgz
2121

2222
docgen/markdown/
2323
service_account.json
24+
dataconnect/

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const FIREBASE_DATA_CONNECT_BASE_URL_FORMAT =
3333

3434
/** The Firebase Data Connect backend base URL format including a connector. */
3535
const FIREBASE_DATA_CONNECT_BASE_URL_FORMAT_WITH_CONNECTOR =
36-
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/${connector}:{endpointId}';
36+
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/{connector}:{endpointId}';
3737

3838
/** Firebase Data Connect base URl format when using the Data Connect emulator. */
3939
const FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT =
@@ -153,6 +153,15 @@ export class DataConnectApiClient {
153153
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
154154
'GraphqlOptions should be a non-null object');
155155
}
156+
//Recent addition
157+
if (typeof options !== 'undefined') {
158+
if (!validator.isNonNullObject(options)) {
159+
throw new FirebaseDataConnectError(
160+
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
161+
'GraphqlOptions must be a non-null object');
162+
}
163+
}
164+
156165
if (!("operationName" in options)) {
157166
throw new FirebaseDataConnectError(
158167
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
@@ -215,7 +224,7 @@ export class DataConnectApiClient {
215224
};
216225
let urlFormat: string;
217226
if (useEmulator()) {
218-
if ('connector' in urlParams){
227+
if ('connector' in urlParams && (endpointId === EXECUTE_QUERY_ENDPOINT || endpointId === EXECUTE_MUTATION_ENDPOINT)){
219228
urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT_WITH_CONNECTOR, {
220229
host: emulatorHost()
221230
});
@@ -226,7 +235,7 @@ export class DataConnectApiClient {
226235
});
227236
}
228237
} else {
229-
if ('connector' in urlParams){
238+
if ('connector' in urlParams && (endpointId === EXECUTE_QUERY_ENDPOINT || endpointId === EXECUTE_MUTATION_ENDPOINT)){
230239
urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT_WITH_CONNECTOR}
231240
else{
232241
urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT;}

src/data-connect/data-connect.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ export class DataConnect {
8484
query: string,
8585
options?: GraphqlOptions<Variables>,
8686
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
87-
if ("connector" in this.connectorConfig){
88-
throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphql does not require a connector');
89-
}
87+
// if ("connector" in this.connectorConfig){
88+
// throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphql does not require a connector');
89+
// }
9090
return this.client.executeGraphql(query, options);
9191
}
9292

@@ -102,9 +102,9 @@ export class DataConnect {
102102
query: string,
103103
options?: GraphqlOptions<Variables>,
104104
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
105-
if ("connector" in this.connectorConfig){
106-
throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphqlRead does not require a connector');
107-
}
105+
// if ("connector" in this.connectorConfig){
106+
// throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphqlRead does not require a connector');
107+
// }
108108
return this.client.executeGraphqlRead(query, options);
109109
}
110110

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

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ describe('DataConnectApiClient', () => {
8181
connectorConfig,
8282
mocks.mockCredentialApp());
8383

84+
const clientWithoutProjectId_with_connector = new DataConnectApiClient(
85+
connectorConfig_with_connector,
86+
mocks.mockCredentialApp());
87+
8488
const mockOptions = {
8589
credential: new mocks.MockCredential(),
8690
projectId: 'test-project',
@@ -89,12 +93,14 @@ describe('DataConnectApiClient', () => {
8993
let app: FirebaseApp;
9094

9195
let apiClient: DataConnectApiClient;
96+
let apiClient_with_connector: DataConnectApiClient;
9297
let sandbox: sinon.SinonSandbox;
9398

9499
beforeEach(() => {
95100
sandbox = sinon.createSandbox();
96101
app = mocks.appWithOptions(mockOptions);
97102
apiClient = new DataConnectApiClient(connectorConfig, app);
103+
apiClient_with_connector = new DataConnectApiClient(connectorConfig_with_connector, app);
98104
});
99105

100106
afterEach(() => {
@@ -116,6 +122,17 @@ describe('DataConnectApiClient', () => {
116122
it('should initialize httpClient with the provided app', () => {
117123
expect((apiClient as any).httpClient).to.be.an.instanceOf(AuthorizedHttpClient);
118124
});
125+
// Test for an app instance with a connector within the connector config
126+
it('should throw an error if app is not a valid Firebase app instance', () => {
127+
expect(() => new DataConnectApiClient(connectorConfig_with_connector, null as unknown as FirebaseApp)).to.throw(
128+
FirebaseDataConnectError,
129+
'First argument passed to getDataConnect() must be a valid Firebase app instance.'
130+
);
131+
});
132+
133+
it('should initialize httpClient with the provided app', () => {
134+
expect((apiClient_with_connector as any).httpClient).to.be.an.instanceOf(AuthorizedHttpClient);
135+
});
119136
});
120137

121138
describe('executeGraphql', () => {
@@ -215,7 +232,7 @@ describe('DataConnectApiClient', () => {
215232
method: 'POST',
216233
url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`,
217234
headers: EXPECTED_HEADERS,
218-
data: { query: 'query' }
235+
data: { query: 'query' }
219236
});
220237
});
221238
});
@@ -235,6 +252,164 @@ describe('DataConnectApiClient', () => {
235252
});
236253
});
237254
});
255+
256+
it('should resolve with the GraphQL response on success when a connector is passed in', () => {
257+
interface UsersResponse {
258+
users: [
259+
user: {
260+
id: string;
261+
name: string;
262+
address: string;
263+
}
264+
];
265+
}
266+
const stub = sandbox
267+
.stub(HttpClient.prototype, 'send')
268+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
269+
return apiClient_with_connector.executeGraphql<UsersResponse, unknown>('query', {})
270+
.then((resp) => {
271+
expect(resp.data.users).to.be.not.empty;
272+
expect(resp.data.users[0].name).to.be.not.undefined;
273+
expect(resp.data.users[0].address).to.be.not.undefined;
274+
expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users);
275+
expect(stub).to.have.been.calledOnce.and.calledWith({
276+
method: 'POST',
277+
url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`,
278+
headers: EXPECTED_HEADERS,
279+
data: { query: 'query' }
280+
});
281+
});
282+
});
283+
284+
it('should use DATA_CONNECT_EMULATOR_HOST if set', () => {
285+
process.env.DATA_CONNECT_EMULATOR_HOST = 'localhost:9399';
286+
const stub = sandbox
287+
.stub(HttpClient.prototype, 'send')
288+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
289+
return apiClient_with_connector.executeGraphql('query', {})
290+
.then(() => {
291+
expect(stub).to.have.been.calledOnce.and.calledWith({
292+
method: 'POST',
293+
url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`,
294+
headers: EMULATOR_EXPECTED_HEADERS,
295+
data: { query: 'query' }
296+
});
297+
});
298+
});
299+
300+
});
301+
302+
describe('executeMutation', () => {
303+
//what if there's no project id and also there's no connector, what error would that be? -> should error the connector first because you won't even reach the endpoint to find out if there's a project id or not
304+
it('should reject when project id is not available', () => {
305+
return clientWithoutProjectId_with_connector.executeMutation({operationName: 'getById'})
306+
.should.eventually.be.rejectedWith(noProjectId);
307+
});
308+
309+
it('should throw an error if no arguments are passed in', async () => {
310+
await expect(apiClient_with_connector.executeMutation(undefined as any)).to.be.rejectedWith(
311+
FirebaseDataConnectError,
312+
'GraphqlOptions should be a non-null object'
313+
);
314+
});
315+
316+
const invalidOptions = [null, NaN, 0, 1, true, false, [], _.noop];
317+
invalidOptions.forEach((invalidOption) => {
318+
it('should throw given an invalid options object: ' + JSON.stringify(invalidOption), async () => {
319+
await expect(apiClient_with_connector.executeMutation(invalidOption as any)).to.be.rejectedWith(
320+
FirebaseDataConnectError,
321+
'GraphqlOptions must be a non-null object'
322+
);
323+
});
324+
});
325+
//could this pass as a null object, also what if the wrong operaton was passed in, would it be handled in another test- say integration?
326+
it('should throw an error if there is no operationName', async () => {
327+
await expect(apiClient_with_connector.executeMutation({})).to.be.rejectedWith(
328+
FirebaseDataConnectError,
329+
'GraphqlOptions must contain `operationName`.'
330+
);
331+
});
332+
333+
it('should reject when a full platform error response is received', () => {
334+
sandbox
335+
.stub(HttpClient.prototype, 'send')
336+
.rejects(utils.errorFrom(ERROR_RESPONSE, 404));
337+
const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found');
338+
return apiClient_with_connector.executeMutation({operationName: 'getById'})
339+
.should.eventually.be.rejected.and.deep.include(expected);
340+
});
341+
342+
it('should reject with unknown-error when error code is not present', () => {
343+
sandbox
344+
.stub(HttpClient.prototype, 'send')
345+
.rejects(utils.errorFrom({}, 404));
346+
const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}');
347+
return apiClient_with_connector.executeMutation({operationName: 'getById'})
348+
.should.eventually.be.rejected.and.deep.include(expected);
349+
});
350+
351+
it('should reject with unknown-error for non-json response', () => {
352+
sandbox
353+
.stub(HttpClient.prototype, 'send')
354+
.rejects(utils.errorFrom('not json', 404));
355+
const expected = new FirebaseDataConnectError(
356+
'unknown-error', 'Unexpected response with status: 404 and body: not json');
357+
return apiClient_with_connector.executeMutation({operationName: 'getById'})
358+
.should.eventually.be.rejected.and.deep.include(expected);
359+
});
360+
361+
it('should reject when rejected with a FirebaseDataConnectError', () => {
362+
const expected = new FirebaseDataConnectError('internal-error', 'socket hang up');
363+
sandbox
364+
.stub(HttpClient.prototype, 'send')
365+
.rejects(expected);
366+
return apiClient_with_connector.executeMutation({operationName: 'getById'})
367+
.should.eventually.be.rejected.and.deep.include(expected);
368+
});
369+
370+
it('should resolve with the Mutation response on success', () => {
371+
interface UsersResponse {
372+
users: [
373+
user: {
374+
id: string;
375+
name: string;
376+
address: string;
377+
}
378+
];
379+
}
380+
const stub = sandbox
381+
.stub(HttpClient.prototype, 'send')
382+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
383+
return apiClient_with_connector.executeMutation<UsersResponse, unknown>({operationName: 'getById'})
384+
.then((resp) => {
385+
expect(resp.data.users).to.be.not.empty;
386+
expect(resp.data.users[0].name).to.be.not.undefined;
387+
expect(resp.data.users[0].address).to.be.not.undefined;
388+
expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users);
389+
expect(stub).to.have.been.calledOnce.and.calledWith({
390+
method: 'POST',
391+
url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`,
392+
headers: EXPECTED_HEADERS,
393+
data: { query: undefined, name: 'getById', operationName: 'getById' }
394+
});
395+
});
396+
});
397+
398+
it('should use DATA_CONNECT_EMULATOR_HOST if set', () => {
399+
process.env.DATA_CONNECT_EMULATOR_HOST = 'localhost:9399';
400+
const stub = sandbox
401+
.stub(HttpClient.prototype, 'send')
402+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
403+
return apiClient_with_connector.executeMutation({operationName: 'getById'})
404+
.then(() => {
405+
expect(stub).to.have.been.calledOnce.and.calledWith({
406+
method: 'POST',
407+
url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`,
408+
headers: EMULATOR_EXPECTED_HEADERS,
409+
data: { query: undefined, name: 'getById', operationName: 'getById' }
410+
});
411+
});
412+
});
238413
});
239414
});
240415

0 commit comments

Comments
 (0)