Skip to content

Instantly share code, notes, and snippets.

@ryanmeisters
Last active May 9, 2022 00:11
Show Gist options
  • Save ryanmeisters/05a1aae411ed96f845b6c0b5073751ff to your computer and use it in GitHub Desktop.
Save ryanmeisters/05a1aae411ed96f845b6c0b5073751ff to your computer and use it in GitHub Desktop.
Example of mocking firebase callable function authentication for testing
import test, { before as beforeAllInFile, after as afterAllInFile } from 'ava';
import { deactivateCustomer, DwollaTransferStatus } from '../src/util/dwolla';
import { TEST_USER_ID, TEST_USER_EMAIL } from './helper/mockAuth';
import { getTestPlaidLinkToken } from './helper/plaid';
import { testFunctions, clearEmulatorDatabase } from './helper/firebase';
import { getTestUserFundingSources, getTestUserPrivateDocumentData, getTestUserTotalBalance, getTestUserClearedBalance } from './helper/user-data';
import { getFirstDepositTransactionData } from './helper/transactions';
import { hasShape, verifyShape } from './helper/assert/joi-schema';
import { eventually } from './helper/assert/eventually';
import * as Joi from '@hapi/joi';
import { TransactionType } from '../src/models/TransactionType';
import { FundingSourceAttributes } from '../src/models/FundingSource';
/// Hi! These are integrations tests that verify our integration with Plaid/Dwolla
/// against the firebase emulators.
/// Make sure to run them with `ava --serial`!
// We save the URL of the dwolla customer created for the integration tests so
// that we can deactivate it after the tests (and recreate next time)
let customerUrl: string|undefined;
const TEST_DEPOSIT_AMOUNT = 8;
beforeAllInFile('Dwolla Integration Tests Setup', async (t) => {
const { linkToken: token, accountId } = await getTestPlaidLinkToken();
await testFunctions.httpsCallable('linkAccount')({ token, accountId });
});
afterAllInFile.always('Guaranteed Cleanup', async (t) => {
await clearEmulatorDatabase();
if (customerUrl) {
await deactivateCustomer(customerUrl);
console.log(`Deactivated customer: ${customerUrl}`);
}
});
test('linkAccount function should create a dwolla customer for the authenticated user', async (t) => {
const userData = (await getTestUserPrivateDocumentData())!;
verifyShape(t, userData.dwollaCustomer, {
id: Joi.string(),
link: Joi.string().uri(),
email: TEST_USER_EMAIL
});
// Save so we can deactivate the customer after all tests
customerUrl = userData.dwollaCustomer.link;
});
test('link account function should save the users payment source', async (t) => {
const sources = await getTestUserFundingSources();
t.assert(sources.size === 1, 'There should be 1 funding source for the user');
verifyShape(t, sources.docs[0].data(), {
[FundingSourceAttributes.dwollaId]: Joi.string(),
[FundingSourceAttributes.dwollaLink]: Joi.string().uri()
});
});
test('deposit function writes a transaction to database', async (t) => {
await testFunctions.httpsCallable('deposit')({ amount: TEST_DEPOSIT_AMOUNT });
const transaction = await getFirstDepositTransactionData();
if (!transaction) { t.fail('There should be one deposit transaction in the db'); }
verifyShape(t, transaction, {
amount: TEST_DEPOSIT_AMOUNT,
jackpotId: null, // TEST_JACKPOT_ID, -- disabled b/c jackpot query not working in emulator
userId: TEST_USER_ID,
type: TransactionType.deposit,
dwollaId: null,
dwollaLink: null,
status: null,
createdAt: Joi.object(),
clearedAt: Joi.object().allow(null)
});
});
test('deposit adds to the users total balance', async (t) => {
const totalBalance = await getTestUserTotalBalance();
t.assert(totalBalance === TEST_DEPOSIT_AMOUNT);
});
test('deposit does not immediately add to the users cleared balance', async (t) => {
const clearedBalance = await getTestUserClearedBalance();
t.assert(clearedBalance === undefined);
});
test('writing deposit transaction to database causes transaction to be created on Dwolla', async (t) => {
return eventually(async () => {
const transaction = (await getFirstDepositTransactionData())!;
hasShape(transaction, {
dwollaId: Joi.string(),
dwollaLink: Joi.string().uri(),
status: DwollaTransferStatus.pending,
}, { allowUnknown: true });
})
.then(() => t.pass())
.catch(e => t.fail(e));
});
import * as firebaseTesting from '@firebase/testing';
const PROJECT_ID = 'sweepsapp-stage';
const DATABASE_NAME = 'test-db'; // Do the test apps even use this?
const testApp = firebaseTesting.initializeTestApp({
projectId: PROJECT_ID,
databaseName: DATABASE_NAME,
// Auth doesn't work with callable fuctions, see https://github.com/firebase/firebase-tools/issues/1475
// So it's mocked in the actual application code for integration tests
// auth: { uid: 'tets-user', token: 'owner', email: '[email protected]' }
});
export const testDb = testApp.firestore();
export const testFunctions = testApp.functions();
testFunctions.useFunctionsEmulator('http://localhost:5001');
const adminApp = firebaseTesting.initializeAdminApp({
projectId: PROJECT_ID,
databaseName: DATABASE_NAME
});
export const testAdminDb = adminApp.firestore();
export async function clearEmulatorDatabase() {
await firebaseTesting.clearFirestoreData({ projectId: PROJECT_ID });
}
import { AuthUser } from "../models/User";
import { mockFirebaseAuth } from "../../integration-tests/helper/mock-auth";
export const isIntegrationTesting = process.env.FUNCTIONS_EMULATOR;
/**
* This exists becuase we are not yet able to pass mock authentication to the
* function emulator from the @firebase/testing library.
* See: https://github.com/firebase/firebase-tools/issues/1475
*/
export function authFromFunctionContext(context: any): AuthUser {
if (isIntegrationTesting) {
console.log("Authentication is mocked for integration testing");
}
return isIntegrationTesting
? mockFirebaseAuth
: ((<unknown>context.auth) as AuthUser);
}
import * as functions from "firebase-functions";
import * as Joi from "@hapi/joi";
import { HttpsError } from "firebase-functions/lib/providers/https";
import { getUser } from "../../firestore/user";
import { validate } from "../../util/validation";
import { deposit as doDeposit } from "./deposit";
import { authFromFunctionContext } from "../../util/function-auth";
import { inspect } from "util";
interface DepositBody {
amount: number;
}
const depositBody = {
amount: Joi.number()
.min(1)
.max(5000)
.required()
};
export const deposit = functions.https.onCall(
async (data: DepositBody, context) => {
const authUser = authFromFunctionContext(context);
try {
validate(data, depositBody);
const user = await getUser(authUser);
const { amount } = data;
await doDeposit(user, amount);
} catch (error) {
console.error(inspect(error, undefined, 10, true));
throw new HttpsError("internal", "Something went wrong");
}
return {};
}
);
export const TEST_USER_ID = 'integration-test-user-id';
export const TEST_USER_EMAIL = '[email protected]';
export const TEST_USER_NAME = 'Integration Test User';
export const mockFirebaseAuth = {
uid: TEST_USER_ID,
token: {
name: TEST_USER_NAME,
email: TEST_USER_EMAIL
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment