Skip to content

Commit

Permalink
Modernize integration test (#1074)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
taeold committed Apr 13, 2022
1 parent 60a7a40 commit 5f8770f
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 226 deletions.
227 changes: 100 additions & 127 deletions integration_test/functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,118 @@
import { PubSub } from '@google-cloud/pubsub';
import { Request, Response } from 'express';
import fetch from 'node-fetch';
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import * as fs from 'fs';
import * as https from 'https';

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

import * as utils from './test-utils';
import * as testLab from './testLab-utils';
import * as testLab from './v1/testLab-utils';

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

// TODO(klimt): Get rid of this once the JS client SDK supports callable triggers.
function callHttpsTrigger(name: string, data: any, baseUrl) {
return utils.makeRequest(
function callHttpsTrigger(name: string, data: any) {
return fetch(
`https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/${name}`,
{
method: 'POST',
host: REGION + '-' + firebaseConfig.projectId + '.' + baseUrl,
path: '/' + name,
headers: {
'Content-Type': 'application/json',
},
},
JSON.stringify({ data })
body: JSON.stringify({ data }),
}
);
}

async function callScheduleTrigger(functionName: string, region: string) {
const accessToken = await admin.credential
.applicationDefault()
.getAccessToken();
return new Promise<string>((resolve, reject) => {
const request = https.request(
{
method: 'POST',
host: 'cloudscheduler.googleapis.com',
path: `/v1/projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken.access_token}`,
},
const response = await fetch(
`https://cloudscheduler.googleapis.com/v1/projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken.access_token}`,
},
(response) => {
if (response.statusCode! / 100 != 2) {
reject(
new Error('Failed request with status ' + response.statusCode!)
);
return;
}
let body = '';
response.on('data', (chunk) => {
body += chunk;
});
response.on('end', () => {
console.log(`Successfully scheduled function ${functionName}`);
resolve(body);
});
}
);
request.on('error', (err) => {
console.error('Failed to schedule cloud scheduler job with error', err);
reject(err);
});
request.write('{}');
request.end();
});
}
);
if (!response.ok) {
throw new Error(`Failed request with status ${response.status}!`);
}
const data = await response.text();
functions.logger.log(`Successfully scheduled function ${functionName}`, data);
return;
}

async function updateRemoteConfig(
testId: string,
accessToken: string
): Promise<void> {
await fetch(
`https://firebaseremoteconfig.googleapis.com/v1/projects/${firebaseConfig.projectId}/remoteConfig`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json; UTF-8',
'Accept-Encoding': 'gzip',
'If-Match': '*',
},
body: JSON.stringify({ version: { description: testId } }),
}
);
}

function v1Tests(testId: string, accessToken: string) {
return [
// A database write to trigger the Firebase Realtime Database tests.
admin
.database()
.ref(`dbTests/${testId}/start`)
.set({ '.sv': 'timestamp' }),
// A Pub/Sub publish to trigger the Cloud Pub/Sub tests.
new PubSub()
.topic('pubsubTests')
.publish(Buffer.from(JSON.stringify({ testId }))),
// A user creation to trigger the Firebase Auth user creation tests.
admin
.auth()
.createUser({
email: `${testId}@fake.com`,
password: 'secret',
displayName: `${testId}`,
})
.then((userRecord) => {
// A user deletion to trigger the Firebase Auth user deletion tests.
admin.auth().deleteUser(userRecord.uid);
}),
// A firestore write to trigger the Cloud Firestore tests.
admin
.firestore()
.collection('tests')
.doc(testId)
.set({ test: testId }),
// Invoke a callable HTTPS trigger.
callHttpsTrigger('v1-callableTests', { foo: 'bar', testId }),
// A Remote Config update to trigger the Remote Config tests.
updateRemoteConfig(testId, accessToken),
// A storage upload to trigger the Storage tests
admin
.storage()
.bucket()
.upload('/tmp/' + testId + '.txt'),
testLab.startTestRun(firebaseConfig.projectId, testId),
// Invoke the schedule for our scheduled function to fire
callScheduleTrigger('v1-schedule', 'us-central1'),
];
}

export const integrationTests: any = functions
Expand All @@ -86,12 +121,6 @@ export const integrationTests: any = functions
timeoutSeconds: 540,
})
.https.onRequest(async (req: Request, resp: Response) => {
// We take the base url for our https call (cloudfunctions.net, txckloud.net, etc) from the request
// so that it changes with the environment that the tests are run in
const baseUrl = req.hostname
.split('.')
.slice(1)
.join('.');
const testId = admin
.database()
.ref()
Expand All @@ -101,71 +130,15 @@ export const integrationTests: any = functions
.ref(`testRuns/${testId}/timestamp`)
.set(Date.now());
const testIdRef = admin.database().ref(`testRuns/${testId}`);
console.log('testId is: ', testId);
functions.logger.info('testId is: ', testId);
fs.writeFile('/tmp/' + testId + '.txt', 'test', () => {});
try {
await Promise.all([
// A database write to trigger the Firebase Realtime Database tests.
admin
.database()
.ref(`dbTests/${testId}/start`)
.set({ '.sv': 'timestamp' }),
// A Pub/Sub publish to trigger the Cloud Pub/Sub tests.
new PubSub()
.topic('pubsubTests')
.publish(Buffer.from(JSON.stringify({ testId }))),
// A user creation to trigger the Firebase Auth user creation tests.
admin
.auth()
.createUser({
email: `${testId}@fake.com`,
password: 'secret',
displayName: `${testId}`,
})
.then((userRecord) => {
// A user deletion to trigger the Firebase Auth user deletion tests.
admin.auth().deleteUser(userRecord.uid);
}),
// A firestore write to trigger the Cloud Firestore tests.
admin
.firestore()
.collection('tests')
.doc(testId)
.set({ test: testId }),
// Invoke a callable HTTPS trigger.
callHttpsTrigger('callableTests', { foo: 'bar', testId }, baseUrl),
// A Remote Config update to trigger the Remote Config tests.
admin.credential
.applicationDefault()
.getAccessToken()
.then((accessToken) => {
const options = {
hostname: 'firebaseremoteconfig.googleapis.com',
path: `/v1/projects/${firebaseConfig.projectId}/remoteConfig`,
method: 'PUT',
headers: {
Authorization: 'Bearer ' + accessToken.access_token,
'Content-Type': 'application/json; UTF-8',
'Accept-Encoding': 'gzip',
'If-Match': '*',
},
};
const request = https.request(options, (resp) => {});
request.write(JSON.stringify({ version: { description: testId } }));
request.end();
}),
// A storage upload to trigger the Storage tests
admin
.storage()
.bucket()
.upload('/tmp/' + testId + '.txt'),
testLab.startTestRun(firebaseConfig.projectId, testId),
// Invoke the schedule for our scheduled function to fire
callScheduleTrigger('schedule', 'us-central1'),
]);

const accessToken = await admin.credential
.applicationDefault()
.getAccessToken();
await Promise.all([...v1Tests(testId, accessToken.access_token)]);
// On test completion, check that all tests pass and reply "PASS", or provide further details.
console.log('Waiting for all tests to report they pass...');
functions.logger.info('Waiting for all tests to report they pass...');
await new Promise<void>((resolve, reject) => {
setTimeout(() => reject(new Error('Timeout')), 5 * 60 * 1000);
let testsExecuted = 0;
Expand All @@ -179,7 +152,7 @@ export const integrationTests: any = functions
);
return;
}
console.log(
functions.logger.info(
`${snapshot.key} passed (${testsExecuted} of ${numTests})`
);
if (testsExecuted < numTests) {
Expand All @@ -190,10 +163,10 @@ export const integrationTests: any = functions
resolve();
});
});
console.log('All tests pass!');
functions.logger.info('All tests pass!');
resp.status(200).send('PASS \n');
} catch (err) {
console.log(`Some tests failed: ${err}`);
functions.logger.info(`Some tests failed: ${err}`, err);
resp
.status(500)
.send(
Expand Down
38 changes: 0 additions & 38 deletions integration_test/functions/src/test-utils.ts

This file was deleted.

10 changes: 5 additions & 5 deletions integration_test/functions/src/testing.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as firebase from 'firebase-admin';
import { EventContext } from 'firebase-functions';
import * as functions from 'firebase-functions';

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

run(testId: string, data: T, context?: EventContext): Promise<any> {
run(testId: string, data: T, context?: functions.EventContext): Promise<any> {
const running: Array<Promise<any>> = [];
for (const testName in this.tests) {
if (!this.tests.hasOwnProperty(testName)) {
Expand All @@ -30,7 +30,7 @@ export class TestSuite<T> {
.then(() => this.tests[testName](data, context))
.then(
(result) => {
console.log(
functions.logger.info(
`${result ? 'Passed' : 'Failed with successful op'}: ${testName}`
);
return { name: testName, passed: !!result };
Expand All @@ -47,7 +47,7 @@ export class TestSuite<T> {
results.forEach((val) => (sum = sum + val.passed));
const summary = `passed ${sum} of ${running.length}`;
const passed = sum === running.length;
console.log(summary);
functions.logger.info(summary);
const result = { passed, summary, tests: results };
return firebase
.database()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { expectEq, TestSuite } from './testing';
import { expectEq, TestSuite } from '../testing';
import UserMetadata = admin.auth.UserRecord;

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

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

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

0 comments on commit 5f8770f

Please sign in to comment.