Last active
May 9, 2022 00:11
-
-
Save ryanmeisters/05a1aae411ed96f845b6c0b5073751ff to your computer and use it in GitHub Desktop.
Example of mocking firebase callable function authentication for testing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}; | |
} | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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