Skip to content

Commit 5f8770f

Browse files
authored
Modernize integration test (#1074)
Small touchups to the existing integration test: 1) Use `node-fetch` to simplify HTTP calls 2) Group v1 triggers (in prep for running v2 trigger tests) 3) Replace use of `console.log` to `functions.logger.info` 4) Get rid of node10 as test target
1 parent 60a7a40 commit 5f8770f

15 files changed

+159
-226
lines changed

integration_test/functions/src/index.ts

+100-127
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,118 @@
11
import { PubSub } from '@google-cloud/pubsub';
22
import { Request, Response } from 'express';
3+
import fetch from 'node-fetch';
34
import * as admin from 'firebase-admin';
45
import * as functions from 'firebase-functions';
56
import * as fs from 'fs';
6-
import * as https from 'https';
77

8-
export * from './pubsub-tests';
9-
export * from './database-tests';
10-
export * from './auth-tests';
11-
export * from './firestore-tests';
12-
export * from './https-tests';
13-
export * from './remoteConfig-tests';
14-
export * from './storage-tests';
15-
export * from './testLab-tests';
16-
const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test.
8+
import * as v1 from './v1/index';
9+
const numTests = Object.keys(v1).filter((k) =>
10+
({}.hasOwnProperty.call(v1[k], '__endpoint'))
11+
).length;
12+
export { v1 };
1713

18-
import * as utils from './test-utils';
19-
import * as testLab from './testLab-utils';
14+
import * as testLab from './v1/testLab-utils';
2015

21-
import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213)
22-
import { config } from 'firebase-functions';
2316
const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG);
24-
admin.initializeApp();
2517
const REGION = functions.config().functions.test_region;
18+
admin.initializeApp();
2619

27-
// TODO(klimt): Get rid of this once the JS client SDK supports callable triggers.
28-
function callHttpsTrigger(name: string, data: any, baseUrl) {
29-
return utils.makeRequest(
20+
function callHttpsTrigger(name: string, data: any) {
21+
return fetch(
22+
`https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/${name}`,
3023
{
3124
method: 'POST',
32-
host: REGION + '-' + firebaseConfig.projectId + '.' + baseUrl,
33-
path: '/' + name,
3425
headers: {
3526
'Content-Type': 'application/json',
3627
},
37-
},
38-
JSON.stringify({ data })
28+
body: JSON.stringify({ data }),
29+
}
3930
);
4031
}
4132

4233
async function callScheduleTrigger(functionName: string, region: string) {
4334
const accessToken = await admin.credential
4435
.applicationDefault()
4536
.getAccessToken();
46-
return new Promise<string>((resolve, reject) => {
47-
const request = https.request(
48-
{
49-
method: 'POST',
50-
host: 'cloudscheduler.googleapis.com',
51-
path: `/v1/projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`,
52-
headers: {
53-
'Content-Type': 'application/json',
54-
Authorization: `Bearer ${accessToken.access_token}`,
55-
},
37+
const response = await fetch(
38+
`https://cloudscheduler.googleapis.com/v1/projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`,
39+
{
40+
method: 'POST',
41+
headers: {
42+
'Content-Type': 'application/json',
43+
Authorization: `Bearer ${accessToken.access_token}`,
5644
},
57-
(response) => {
58-
if (response.statusCode! / 100 != 2) {
59-
reject(
60-
new Error('Failed request with status ' + response.statusCode!)
61-
);
62-
return;
63-
}
64-
let body = '';
65-
response.on('data', (chunk) => {
66-
body += chunk;
67-
});
68-
response.on('end', () => {
69-
console.log(`Successfully scheduled function ${functionName}`);
70-
resolve(body);
71-
});
72-
}
73-
);
74-
request.on('error', (err) => {
75-
console.error('Failed to schedule cloud scheduler job with error', err);
76-
reject(err);
77-
});
78-
request.write('{}');
79-
request.end();
80-
});
45+
}
46+
);
47+
if (!response.ok) {
48+
throw new Error(`Failed request with status ${response.status}!`);
49+
}
50+
const data = await response.text();
51+
functions.logger.log(`Successfully scheduled function ${functionName}`, data);
52+
return;
53+
}
54+
55+
async function updateRemoteConfig(
56+
testId: string,
57+
accessToken: string
58+
): Promise<void> {
59+
await fetch(
60+
`https://firebaseremoteconfig.googleapis.com/v1/projects/${firebaseConfig.projectId}/remoteConfig`,
61+
{
62+
method: 'PUT',
63+
headers: {
64+
Authorization: `Bearer ${accessToken}`,
65+
'Content-Type': 'application/json; UTF-8',
66+
'Accept-Encoding': 'gzip',
67+
'If-Match': '*',
68+
},
69+
body: JSON.stringify({ version: { description: testId } }),
70+
}
71+
);
72+
}
73+
74+
function v1Tests(testId: string, accessToken: string) {
75+
return [
76+
// A database write to trigger the Firebase Realtime Database tests.
77+
admin
78+
.database()
79+
.ref(`dbTests/${testId}/start`)
80+
.set({ '.sv': 'timestamp' }),
81+
// A Pub/Sub publish to trigger the Cloud Pub/Sub tests.
82+
new PubSub()
83+
.topic('pubsubTests')
84+
.publish(Buffer.from(JSON.stringify({ testId }))),
85+
// A user creation to trigger the Firebase Auth user creation tests.
86+
admin
87+
.auth()
88+
.createUser({
89+
email: `${testId}@fake.com`,
90+
password: 'secret',
91+
displayName: `${testId}`,
92+
})
93+
.then((userRecord) => {
94+
// A user deletion to trigger the Firebase Auth user deletion tests.
95+
admin.auth().deleteUser(userRecord.uid);
96+
}),
97+
// A firestore write to trigger the Cloud Firestore tests.
98+
admin
99+
.firestore()
100+
.collection('tests')
101+
.doc(testId)
102+
.set({ test: testId }),
103+
// Invoke a callable HTTPS trigger.
104+
callHttpsTrigger('v1-callableTests', { foo: 'bar', testId }),
105+
// A Remote Config update to trigger the Remote Config tests.
106+
updateRemoteConfig(testId, accessToken),
107+
// A storage upload to trigger the Storage tests
108+
admin
109+
.storage()
110+
.bucket()
111+
.upload('/tmp/' + testId + '.txt'),
112+
testLab.startTestRun(firebaseConfig.projectId, testId),
113+
// Invoke the schedule for our scheduled function to fire
114+
callScheduleTrigger('v1-schedule', 'us-central1'),
115+
];
81116
}
82117

83118
export const integrationTests: any = functions
@@ -86,12 +121,6 @@ export const integrationTests: any = functions
86121
timeoutSeconds: 540,
87122
})
88123
.https.onRequest(async (req: Request, resp: Response) => {
89-
// We take the base url for our https call (cloudfunctions.net, txckloud.net, etc) from the request
90-
// so that it changes with the environment that the tests are run in
91-
const baseUrl = req.hostname
92-
.split('.')
93-
.slice(1)
94-
.join('.');
95124
const testId = admin
96125
.database()
97126
.ref()
@@ -101,71 +130,15 @@ export const integrationTests: any = functions
101130
.ref(`testRuns/${testId}/timestamp`)
102131
.set(Date.now());
103132
const testIdRef = admin.database().ref(`testRuns/${testId}`);
104-
console.log('testId is: ', testId);
133+
functions.logger.info('testId is: ', testId);
105134
fs.writeFile('/tmp/' + testId + '.txt', 'test', () => {});
106135
try {
107-
await Promise.all([
108-
// A database write to trigger the Firebase Realtime Database tests.
109-
admin
110-
.database()
111-
.ref(`dbTests/${testId}/start`)
112-
.set({ '.sv': 'timestamp' }),
113-
// A Pub/Sub publish to trigger the Cloud Pub/Sub tests.
114-
new PubSub()
115-
.topic('pubsubTests')
116-
.publish(Buffer.from(JSON.stringify({ testId }))),
117-
// A user creation to trigger the Firebase Auth user creation tests.
118-
admin
119-
.auth()
120-
.createUser({
121-
email: `${testId}@fake.com`,
122-
password: 'secret',
123-
displayName: `${testId}`,
124-
})
125-
.then((userRecord) => {
126-
// A user deletion to trigger the Firebase Auth user deletion tests.
127-
admin.auth().deleteUser(userRecord.uid);
128-
}),
129-
// A firestore write to trigger the Cloud Firestore tests.
130-
admin
131-
.firestore()
132-
.collection('tests')
133-
.doc(testId)
134-
.set({ test: testId }),
135-
// Invoke a callable HTTPS trigger.
136-
callHttpsTrigger('callableTests', { foo: 'bar', testId }, baseUrl),
137-
// A Remote Config update to trigger the Remote Config tests.
138-
admin.credential
139-
.applicationDefault()
140-
.getAccessToken()
141-
.then((accessToken) => {
142-
const options = {
143-
hostname: 'firebaseremoteconfig.googleapis.com',
144-
path: `/v1/projects/${firebaseConfig.projectId}/remoteConfig`,
145-
method: 'PUT',
146-
headers: {
147-
Authorization: 'Bearer ' + accessToken.access_token,
148-
'Content-Type': 'application/json; UTF-8',
149-
'Accept-Encoding': 'gzip',
150-
'If-Match': '*',
151-
},
152-
};
153-
const request = https.request(options, (resp) => {});
154-
request.write(JSON.stringify({ version: { description: testId } }));
155-
request.end();
156-
}),
157-
// A storage upload to trigger the Storage tests
158-
admin
159-
.storage()
160-
.bucket()
161-
.upload('/tmp/' + testId + '.txt'),
162-
testLab.startTestRun(firebaseConfig.projectId, testId),
163-
// Invoke the schedule for our scheduled function to fire
164-
callScheduleTrigger('schedule', 'us-central1'),
165-
]);
166-
136+
const accessToken = await admin.credential
137+
.applicationDefault()
138+
.getAccessToken();
139+
await Promise.all([...v1Tests(testId, accessToken.access_token)]);
167140
// On test completion, check that all tests pass and reply "PASS", or provide further details.
168-
console.log('Waiting for all tests to report they pass...');
141+
functions.logger.info('Waiting for all tests to report they pass...');
169142
await new Promise<void>((resolve, reject) => {
170143
setTimeout(() => reject(new Error('Timeout')), 5 * 60 * 1000);
171144
let testsExecuted = 0;
@@ -179,7 +152,7 @@ export const integrationTests: any = functions
179152
);
180153
return;
181154
}
182-
console.log(
155+
functions.logger.info(
183156
`${snapshot.key} passed (${testsExecuted} of ${numTests})`
184157
);
185158
if (testsExecuted < numTests) {
@@ -190,10 +163,10 @@ export const integrationTests: any = functions
190163
resolve();
191164
});
192165
});
193-
console.log('All tests pass!');
166+
functions.logger.info('All tests pass!');
194167
resp.status(200).send('PASS \n');
195168
} catch (err) {
196-
console.log(`Some tests failed: ${err}`);
169+
functions.logger.info(`Some tests failed: ${err}`, err);
197170
resp
198171
.status(500)
199172
.send(

integration_test/functions/src/test-utils.ts

-38
This file was deleted.

integration_test/functions/src/testing.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as firebase from 'firebase-admin';
2-
import { EventContext } from 'firebase-functions';
2+
import * as functions from 'firebase-functions';
33

4-
export type TestCase<T> = (data: T, context?: EventContext) => any;
4+
export type TestCase<T> = (data: T, context?: functions.EventContext) => any;
55
export interface TestCaseMap<T> {
66
[key: string]: TestCase<T>;
77
}
@@ -20,7 +20,7 @@ export class TestSuite<T> {
2020
return this;
2121
}
2222

23-
run(testId: string, data: T, context?: EventContext): Promise<any> {
23+
run(testId: string, data: T, context?: functions.EventContext): Promise<any> {
2424
const running: Array<Promise<any>> = [];
2525
for (const testName in this.tests) {
2626
if (!this.tests.hasOwnProperty(testName)) {
@@ -30,7 +30,7 @@ export class TestSuite<T> {
3030
.then(() => this.tests[testName](data, context))
3131
.then(
3232
(result) => {
33-
console.log(
33+
functions.logger.info(
3434
`${result ? 'Passed' : 'Failed with successful op'}: ${testName}`
3535
);
3636
return { name: testName, passed: !!result };
@@ -47,7 +47,7 @@ export class TestSuite<T> {
4747
results.forEach((val) => (sum = sum + val.passed));
4848
const summary = `passed ${sum} of ${running.length}`;
4949
const passed = sum === running.length;
50-
console.log(summary);
50+
functions.logger.info(summary);
5151
const result = { passed, summary, tests: results };
5252
return firebase
5353
.database()

integration_test/functions/src/auth-tests.ts renamed to integration_test/functions/src/v1/auth-tests.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as admin from 'firebase-admin';
22
import * as functions from 'firebase-functions';
3-
import { expectEq, TestSuite } from './testing';
3+
import { expectEq, TestSuite } from '../testing';
44
import UserMetadata = admin.auth.UserRecord;
55

66
const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';
@@ -10,7 +10,7 @@ export const createUserTests: any = functions
1010
.auth.user()
1111
.onCreate((u, c) => {
1212
const testId: string = u.displayName;
13-
console.log(`testId is ${testId}`);
13+
functions.logger.info(`testId is ${testId}`);
1414

1515
return new TestSuite<UserMetadata>('auth user onCreate')
1616
.it('should have a project as resource', (user, context) =>
@@ -50,7 +50,7 @@ export const deleteUserTests: any = functions
5050
.auth.user()
5151
.onDelete((u, c) => {
5252
const testId: string = u.displayName;
53-
console.log(`testId is ${testId}`);
53+
functions.logger.info(`testId is ${testId}`);
5454

5555
return new TestSuite<UserMetadata>('auth user onDelete')
5656
.it('should have a project as resource', (user, context) =>

0 commit comments

Comments
 (0)