Skip to content

Commit

Permalink
Setup v2 triggers for integration tests (#1075)
Browse files Browse the repository at this point in the history
Also remove region setting option of the test since it's not possible to dynamically set region using function config when parsing triggers via container contract.

Just adding a single v2 trigger def for now - with the structure introduced here, it should be easy to add more triggers in the near future.
  • Loading branch information
taeold committed Apr 13, 2022
1 parent 5f8770f commit b8f1614
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 72 deletions.
95 changes: 76 additions & 19 deletions integration_test/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import * as fs from 'fs';

import * as v1 from './v1/index';
const numTests = Object.keys(v1).filter((k) =>
({}.hasOwnProperty.call(v1[k], '__endpoint'))
).length;
export { v1 };
import * as v1 from './v1';
import * as v2 from './v2';
const getNumTests = (m: object): number => {
return Object.keys(m).filter((k) =>
({}.hasOwnProperty.call(m[k], '__endpoint'))
).length;
};
const numTests = getNumTests(v1) + getNumTests(v2);
export { v1, v2 };

import * as testLab from './v1/testLab-utils';
import { REGION } from './region';

const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG);
const REGION = functions.config().functions.test_region;
admin.initializeApp();

function callHttpsTrigger(name: string, data: any) {
return fetch(
async function callHttpsTrigger(name: string, data: any) {
const resp = await fetch(
`https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/${name}`,
{
method: 'POST',
Expand All @@ -28,19 +32,56 @@ function callHttpsTrigger(name: string, data: any) {
body: JSON.stringify({ data }),
}
);
if (!resp.ok) {
throw Error(resp.statusText);
}
}

async function callV2HttpsTrigger(
name: string,
data: any,
accessToken: string
) {
let resp = await fetch(
`https://cloudfunctions.googleapis.com/v2beta/projects/${firebaseConfig.projectId}/locations/${REGION}/functions/${name}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
if (!resp.ok) {
throw new Error(resp.statusText);
}
const fn = await resp.json();
const uri = fn.serviceConfig?.uri;
if (!uri) {
throw new Error(`Cannot call v2 https trigger ${name} - no uri found`);
}
resp = await fetch(uri, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ data }),
});
if (!resp.ok) {
throw new Error(resp.statusText);
}
}

async function callScheduleTrigger(functionName: string, region: string) {
const accessToken = await admin.credential
.applicationDefault()
.getAccessToken();
async function callScheduleTrigger(
functionName: string,
region: string,
accessToken: string
) {
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}`,
Authorization: `Bearer ${accessToken}`,
},
}
);
Expand All @@ -56,7 +97,7 @@ async function updateRemoteConfig(
testId: string,
accessToken: string
): Promise<void> {
await fetch(
const resp = await fetch(
`https://firebaseremoteconfig.googleapis.com/v1/projects/${firebaseConfig.projectId}/remoteConfig`,
{
method: 'PUT',
Expand All @@ -69,9 +110,12 @@ async function updateRemoteConfig(
body: JSON.stringify({ version: { description: testId } }),
}
);
if (!resp.ok) {
throw new Error(resp.statusText);
}
}

function v1Tests(testId: string, accessToken: string) {
function v1Tests(testId: string, accessToken: string): Promise<void>[] {
return [
// A database write to trigger the Firebase Realtime Database tests.
admin
Expand Down Expand Up @@ -109,9 +153,16 @@ function v1Tests(testId: string, accessToken: string) {
.storage()
.bucket()
.upload('/tmp/' + testId + '.txt'),
testLab.startTestRun(firebaseConfig.projectId, testId),
testLab.startTestRun(firebaseConfig.projectId, testId, accessToken),
// Invoke the schedule for our scheduled function to fire
callScheduleTrigger('v1-schedule', 'us-central1'),
callScheduleTrigger('v1-schedule', 'us-central1', accessToken),
];
}

function v2Tests(testId: string, accessToken: string): Promise<void>[] {
return [
// Invoke a callable HTTPS trigger.
callV2HttpsTrigger('v2-callabletests', { foo: 'bar', testId }, accessToken),
];
}

Expand All @@ -136,15 +187,21 @@ export const integrationTests: any = functions
const accessToken = await admin.credential
.applicationDefault()
.getAccessToken();
await Promise.all([...v1Tests(testId, accessToken.access_token)]);
await Promise.all([
...v1Tests(testId, accessToken.access_token),
...v2Tests(testId, accessToken.access_token),
]);
// On test completion, check that all tests pass and reply "PASS", or provide further details.
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;
testIdRef.on('child_added', (snapshot) => {
if (snapshot.key === 'timestamp') {
return;
}
testsExecuted += 1;
if (snapshot.key != 'timestamp' && !snapshot.val().passed) {
if (!snapshot.val().passed) {
reject(
new Error(
`test ${snapshot.key} failed; see database for details.`
Expand Down
2 changes: 2 additions & 0 deletions integration_test/functions/src/region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// TODO: Add back support for selecting region for integration test once params is ready.
export const REGION = 'us-central1';
3 changes: 1 addition & 2 deletions integration_test/functions/src/v1/auth-tests.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { expectEq, TestSuite } from '../testing';
import { REGION } from '../region';
import UserMetadata = admin.auth.UserRecord;

const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

export const createUserTests: any = functions
.region(REGION)
.auth.user()
Expand Down
2 changes: 1 addition & 1 deletion integration_test/functions/src/v1/database-tests.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { expectEq, expectMatches, TestSuite } from '../testing';
import { REGION } from '../region';
import DataSnapshot = admin.database.DataSnapshot;

const testIdFieldName = 'testId';
const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

export const databaseTests: any = functions
.region(REGION)
Expand Down
2 changes: 1 addition & 1 deletion integration_test/functions/src/v1/firestore-tests.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { expectDeepEq, expectEq, TestSuite } from '../testing';
import { REGION } from '../region';
import DocumentSnapshot = admin.firestore.DocumentSnapshot;

const testIdFieldName = 'documentId';
const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

export const firestoreTests: any = functions
.runWith({
Expand Down
3 changes: 1 addition & 2 deletions integration_test/functions/src/v1/https-tests.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as functions from 'firebase-functions';
import { REGION } from '../region';
import { expectEq, TestSuite } from '../testing';

const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

export const callableTests: any = functions.region(REGION).https.onCall((d) => {
return new TestSuite('https onCall')
.it('should have the correct data', (data: any) =>
Expand Down
3 changes: 1 addition & 2 deletions integration_test/functions/src/v1/pubsub-tests.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { REGION } from '../region';
import { evaluate, expectEq, success, TestSuite } from '../testing';
import PubsubMessage = functions.pubsub.Message;

const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

// TODO(inlined) use multiple queues to run inline.
// Expected message data: {"hello": "world"}
export const pubsubTests: any = functions
Expand Down
3 changes: 1 addition & 2 deletions integration_test/functions/src/v1/remoteConfig-tests.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as functions from 'firebase-functions';
import { REGION } from '../region';
import { expectEq, TestSuite } from '../testing';
import TemplateVersion = functions.remoteConfig.TemplateVersion;

const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

export const remoteConfigTests: any = functions
.region(REGION)
.remoteConfig.onUpdate((v, c) => {
Expand Down
3 changes: 1 addition & 2 deletions integration_test/functions/src/v1/storage-tests.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as functions from 'firebase-functions';
import { REGION } from '../region';
import { expectEq, TestSuite } from '../testing';
import ObjectMetadata = functions.storage.ObjectMetadata;

const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

export const storageTests: any = functions
.runWith({
timeoutSeconds: 540,
Expand Down
2 changes: 1 addition & 1 deletion integration_test/functions/src/v1/testLab-tests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as functions from 'firebase-functions';
import { REGION } from '../region';
import { expectEq, TestSuite } from '../testing';
import TestMatrix = functions.testLab.TestMatrix;
const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1';

export const testLabTests: any = functions
.runWith({
Expand Down
32 changes: 19 additions & 13 deletions integration_test/functions/src/v1/testLab-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,31 @@ const TESTING_API_SERVICE_NAME = 'testing.googleapis.com';
*
* @param projectId Project for which the test run will be created
* @param testId Test id which will be encoded in client info details
* @param accessToken accessToken to attach to requested for authentication
*/
export async function startTestRun(projectId: string, testId: string) {
const accessToken = await admin.credential
.applicationDefault()
.getAccessToken();
export async function startTestRun(
projectId: string,
testId: string,
accessToken: string
) {
const device = await fetchDefaultDevice(accessToken);
return await createTestMatrix(accessToken, projectId, testId, device);
}

async function fetchDefaultDevice(
accessToken: admin.GoogleOAuthAccessToken
): Promise<AndroidDevice> {
const response = await fetch(
async function fetchDefaultDevice(accessToken: string): Promise<AndroidDevice> {
const resp = await fetch(
`https://${TESTING_API_SERVICE_NAME}/v1/testEnvironmentCatalog/ANDROID`,
{
headers: {
Authorization: 'Bearer ' + accessToken.access_token,
Authorization: 'Bearer ' + accessToken,
'Content-Type': 'application/json',
},
}
);
const data = await response.json();
if (!resp.ok) {
throw new Error(resp.statusText);
}
const data = await resp.json();
const models = data?.androidDeviceCatalog?.models || [];
const defaultModels = models.filter(
(m) =>
Expand All @@ -63,7 +66,7 @@ async function fetchDefaultDevice(
}

async function createTestMatrix(
accessToken: admin.GoogleOAuthAccessToken,
accessToken: string,
projectId: string,
testId: string,
device: AndroidDevice
Expand Down Expand Up @@ -95,16 +98,19 @@ async function createTestMatrix(
},
},
};
await fetch(
const resp = await fetch(
`https://${TESTING_API_SERVICE_NAME}/v1/projects/${projectId}/testMatrices`,
{
method: 'POST',
headers: {
Authorization: 'Bearer ' + accessToken.access_token,
Authorization: 'Bearer ' + accessToken,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}
);
if (!resp.ok) {
throw new Error(resp.statusText);
}
return;
}
10 changes: 10 additions & 0 deletions integration_test/functions/src/v2/https-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { onCall } from 'firebase-functions/v2/https';
import { expectEq, TestSuite } from '../testing';

export const callabletests = onCall((req) => {
return new TestSuite('v2 https onCall')
.it('should have the correct data', (data: any) =>
expectEq(data?.foo, 'bar')
)
.run(req.data.testId, req.data);
});
5 changes: 5 additions & 0 deletions integration_test/functions/src/v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { REGION } from '../region';
import { setGlobalOptions } from 'firebase-functions/v2';
setGlobalOptions({ region: REGION });

export * from './https-tests';
Loading

0 comments on commit b8f1614

Please sign in to comment.