Skip to content

Modernize integration test #1074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 98 additions & 127 deletions integration_test/functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,116 @@
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).length; // Assumption: every exported function is its own test.
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 +119,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 +128,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 +150,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 +161,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
Loading