Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1 +1 @@
STRIPE_API_KEY=
STRIPE_API_KEY=
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as admin from 'firebase-admin';
import { DocumentReference, DocumentData } from '@google-cloud/firestore';
import {
waitForDocumentToExistInCollection,
waitForDocumentToExistWithField,
} from './utils';
import { UserRecord } from 'firebase-functions/v1/auth';
import setupEmulator from './setupEmulator';

if (admin.apps.length === 0) {
admin.initializeApp({ projectId: 'demo-project' });
}

setupEmulator();

const firestore = admin.firestore();

function customerCollection() {
return firestore.collection('customers');
}

function paymentsCollection(userId) {
return firestore.collection('customers').doc(userId).collection('payments');
}

export async function findCustomerInCollection(user: UserRecord) {
const doc = firestore.collection('customers').doc(user.uid);

const customerDoc = await waitForDocumentToExistWithField(
doc,
'stripeId',
60000
);

return Promise.resolve({ docId: user.uid, ...customerDoc.data() });
}

export async function findCustomerPaymentInCollection(
userId: string,
stripeId: string
) {
const paymentDoc: DocumentData = await waitForDocumentToExistInCollection(
paymentsCollection(userId),
'customer',
stripeId
);

const paymentRef = paymentsCollection(userId).doc(paymentDoc.doc.id);

const updatedPaymentDoc = await waitForDocumentToExistWithField(
paymentRef,
'prices'
);

return updatedPaymentDoc.data();
}

export async function createCheckoutSession(userId, subscription) {
const checkoutSessionCollection = customerCollection()
.doc(userId)
.collection('checkout_sessions');

const checkoutSessionDocument: DocumentReference =
await checkoutSessionCollection.add({
success_url: 'http://test.com/success',
cancel_url: 'http://test.com/cancel',
...subscription,
});

const checkoutSessionDoc = await waitForDocumentToExistWithField(
checkoutSessionDocument,
'created'
);

return checkoutSessionDoc.data();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import puppeteer from 'puppeteer';

export default async (url: string): Promise<void> => {
console.info('Running checkout form in headless mode...');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 120000,
});

await page.focus('#cardNumber');
await page.keyboard.type('4242424242424242', { delay: 100 });
await page.keyboard.press('Enter');

await page.focus('#cardExpiry');
await page.keyboard.type('1224');

await page.focus('#cardCvc');
await page.keyboard.type('123');

await page.focus('#billingName');
await page.keyboard.type('testing');

await page.focus('#billingAddressLine1');
await page.keyboard.type('1600 Amphitheatre Parkwa');
await page.keyboard.press('Enter');

await page.focus('#billingLocality');
await page.keyboard.type('Mountain View');

await page.focus('#billingPostalCode');
await page.keyboard.type('CA 94043');
await page.keyboard.press('Enter');

await page.waitForNetworkIdle();
await browser.close();
} catch (exception) {
} finally {
await browser.close();
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { faker } from '@faker-js/faker';
import config from '../../../lib/config';
import { Product, Subscription } from '../../../src/interfaces';

const stripe = require('stripe')(config.stripeSecretKey);

export const createRandomSubscription = async (
customer
): Promise<Subscription> => {
const name = faker.commerce.product();

/** create a product */
const product: Product = await stripe.products.create({
name,
description: `Description for ${name}`,
});

/** create a price */
const price = await stripe.prices.create({
unit_amount: 1000,
currency: 'gbp',
recurring: { interval: 'month' },
product: product.id,
});

/** create payment method */
const paymentMethod = await stripe.paymentMethods.create({
type: 'card',
card: {
number: '4242424242424242',
exp_month: 5,
exp_year: 2023,
cvc: '314',
},
});

/** attach payment method to customer */
await stripe.paymentMethods.attach(paymentMethod.id, { customer });
await stripe.customers.update(customer, {
invoice_settings: { default_payment_method: paymentMethod.id },
});

/** Create a product */
const subscription: Subscription = await stripe.subscriptions.create({
customer,
items: [{ price: price.id }],
payment_settings: {
payment_method_types: ['card'],
},
});

return Promise.resolve(subscription);
};
19 changes: 16 additions & 3 deletions firestore-stripe-payments/functions/__tests__/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export const waitForDocumentToExistWithField = (
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
reject(new Error('Timeout waiting for firestore document'));
reject(
new Error(
`Timeout waiting for firestore document to exist with field ${field}`
)
);
}, timeout);
const unsubscribe = document.onSnapshot(async (snapshot: DocumentData) => {
if (snapshot.exists && snapshot.data()[field]) {
Expand All @@ -54,7 +58,11 @@ export const waitForDocumentUpdate = (
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
reject(new Error('Timeout waiting for firestore document'));
reject(
new Error(
`Timeout waiting for firestore document to update with ${field}`
)
);
}, timeout);
const unsubscribe = document.onSnapshot(async (snapshot: DocumentData) => {
if (snapshot.exists && snapshot.data()[field] === value) {
Expand All @@ -78,8 +86,13 @@ export const waitForDocumentToExistInCollection = (
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
reject(new Error('Timeout waiting for firestore document'));
reject(
new Error(
`Timeout waiting for firestore document to exist with field ${field} in collection`
)
);
}, timeout);

const unsubscribe = query.onSnapshot(async (snapshot) => {
const docs = snapshot.docChanges();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as admin from 'firebase-admin';
import { DocumentReference, DocumentData } from '@google-cloud/firestore';
import { UserRecord } from 'firebase-functions/v1/auth';
import setupEmulator from './helpers/setupEmulator';
import { generateRecurringPrice } from './helpers/setupProducts';
import setupEmulator from '../../helpers/setupEmulator';
import { generateRecurringPrice } from '../../helpers/setupProducts';
import {
createFirebaseUser,
waitForDocumentToExistInCollection,
waitForDocumentToExistWithField,
} from './helpers/utils';
} from '../../helpers/utils';

admin.initializeApp({ projectId: 'demo-project' });
setupEmulator();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as admin from 'firebase-admin';
import runCheckout from '../../helpers/forms/runCheckout';

import { UserRecord } from 'firebase-functions/v1/auth';
import { Subscription } from '../../../src/interfaces';
import setupEmulator from '../../helpers/setupEmulator';
import { createRandomSubscription } from '../../helpers/stripeApi/subscriptions';
import { createFirebaseUser } from '../../helpers/utils';

import {
findCustomerInCollection,
createCheckoutSession,
findCustomerPaymentInCollection,
} from '../../helpers/collections';

if (admin.apps.length === 0) {
admin.initializeApp({ projectId: 'demo-project' });
}

setupEmulator();

describe('createSubscriptionCheckoutSession', () => {
let user: UserRecord;

beforeEach(async () => {
user = await createFirebaseUser();
});

afterEach(async () => {
await admin.auth().deleteUser(user.uid);
});

describe('using a web client', () => {
test('successfully creates a subscription based checkout session', async () => {
/** find the customer document */
const { docId, stripeId } = await findCustomerInCollection(user);

/** create a new subscription */
const stripeSubscription: Subscription = await createRandomSubscription(
stripeId
);

/** create a new checkout session */
const { client, success_url, url } = await createCheckoutSession(docId, {
line_items: [
{
//@ts-ignore
price: stripeSubscription.items.data[0].price.id,
quantity: 1,
},
],
});

expect(client).toBe('web');
expect(success_url).toBe('http://test.com/success');

/** complete the checkout fortm */
await runCheckout(url);

/** find user payment */
const { prices } = await findCustomerPaymentInCollection(docId, stripeId);

/** extract prices from array */
const priceRef = await prices[0].get();
const price = priceRef.id;

/** assert values */
//@ts-ignore
expect(price).toEqual(stripeSubscription.items.data[0].price.id);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as admin from 'firebase-admin';
import { DocumentData } from '@google-cloud/firestore';

import setupEmulator from './helpers/setupEmulator';
import setupEmulator from '../../helpers/setupEmulator';
import { UserRecord } from 'firebase-functions/v1/auth';
import {
createFirebaseUser,
waitForDocumentToExistInCollection,
} from './helpers/utils';
} from '../../helpers/utils';

admin.initializeApp({ projectId: 'demo-project' });
setupEmulator();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as admin from 'firebase-admin';
import { DocumentData } from '@google-cloud/firestore';
import setupEmulator from './helpers/setupEmulator';
import { findCustomer } from './helpers/stripeApi/customers';
import setupEmulator from './../../helpers/setupEmulator';
import { findCustomer } from './../../helpers/stripeApi/customers';
import {
repeat,
waitForDocumentToExistWithField,
waitForDocumentToExistInCollection,
createFirebaseUser,
} from './helpers/utils';
} from './../../helpers/utils';
import { UserRecord } from 'firebase-functions/v1/auth';

admin.initializeApp({ projectId: 'demo-project' });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as admin from 'firebase-admin';
import { DocumentData } from '@google-cloud/firestore';
import functions from 'firebase-functions-test';
import * as cloudFunctions from '../src';
import setupEmulator from './helpers/setupEmulator';
import * as cloudFunctions from '../../../src';
import setupEmulator from '../../helpers/setupEmulator';

import {
createFirebaseUser,
waitForDocumentToExistInCollection,
waitForDocumentToExistWithField,
} from './helpers/utils';
} from '../../helpers/utils';
import { UserRecord } from 'firebase-functions/v1/auth';

const testEnv = functions({ projectId: 'demo-project' });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as admin from 'firebase-admin';
import { DocumentData } from '@google-cloud/firestore';
import { Product } from '../src/interfaces';
import setupEmulator from './helpers/setupEmulator';
import { Product } from '../../../src/interfaces';
import setupEmulator from '../../helpers/setupEmulator';

import { createRandomProduct, updateProduct } from './helpers/setupProducts';
import {
createRandomProduct,
updateProduct,
} from '../../helpers/setupProducts';
import {
waitForDocumentToExistInCollection,
waitForDocumentUpdate,
} from './helpers/utils';
} from '../../helpers/utils';

admin.initializeApp({ projectId: 'demo-project' });
setupEmulator();
Expand Down
Loading