Skip to content

Commit a90ac0e

Browse files
authored
chore(firestore-stripe-payments): added initial test setup and basic tests (#364)
1 parent 1f2fe13 commit a90ac0e

31 files changed

+863
-71
lines changed

.github/workflows/test.yml

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
1-
name: Test
1+
name: Testing
22

33
on:
4+
push:
5+
branches:
6+
- "**"
47
pull_request:
5-
paths:
6-
- "firestore-stripe-web-sdk/**"
78
branches:
89
- "**"
910

1011
jobs:
11-
testing:
12+
nodejs:
1213
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
node: ["12", "14"]
17+
name: node.js_${{ matrix.node }}_test
1318
steps:
14-
- uses: actions/checkout@v1
15-
- name: NPM Install
16-
run: npm i
17-
- name: Unit Test
18-
run: lerna run --scope @stripe/firestore-stripe-payments test
19-
- name: API Report
20-
run: lerna run --scope @stripe/firestore-stripe-payments api-extractor
19+
- uses: actions/checkout@v2
20+
- name: Setup node
21+
uses: actions/setup-node@v1
22+
with:
23+
node-version: ${{ matrix.node }}
24+
- name: NPM install
25+
run: npm install
26+
- name: Install firebase CLI
27+
uses: nick-invision/retry@v1
28+
with:
29+
timeout_minutes: 10
30+
retry_wait_seconds: 60
31+
max_attempts: 3
32+
command: npm i -g firebase-tools
33+
- name: Use extension commands
34+
run: firebase --open-sesame extdev
35+
- name: mask env paramaters
36+
run: echo "::add-mask::$STRIPE_WEBHOOK_SECRET"
37+
- name: Run tests with coverage
38+
run: npm run test
39+
env:
40+
STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }}

firestore-stripe-invoices/functions/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"clean": "rimraf lib",
77
"compile": "tsc",
88
"generate-readme": "firebase ext:info .. --markdown > ../README.md",
9-
"test": "echo \"Error: no test specified\" && exit 1"
9+
"test": "echo \"Error: no test specified\" && exit 0"
1010
},
1111
"author": "Stripe (https://stripe.com/)",
1212
"license": "Apache-2.0",
@@ -25,4 +25,4 @@
2525
"printWidth": 80
2626
},
2727
"private": true
28-
}
28+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as admin from 'firebase-admin';
2+
import { DocumentReference, DocumentData } from '@google-cloud/firestore';
3+
import { UserRecord } from 'firebase-functions/v1/auth';
4+
import setupEmulator from './helpers/setupEmulator';
5+
import { generateRecurringPrice } from './helpers/setupProducts';
6+
import {
7+
createFirebaseUser,
8+
waitForDocumentToExistInCollection,
9+
waitForDocumentToExistWithField,
10+
} from './helpers/utils';
11+
12+
admin.initializeApp({ projectId: 'demo-project' });
13+
setupEmulator();
14+
15+
const firestore = admin.firestore();
16+
17+
describe('createCheckoutSession', () => {
18+
let user: UserRecord;
19+
let price = null;
20+
21+
beforeEach(async () => {
22+
price = await generateRecurringPrice();
23+
user = await createFirebaseUser();
24+
});
25+
26+
afterEach(async () => {
27+
await admin.auth().deleteUser(user.uid);
28+
});
29+
30+
describe('using a web client', () => {
31+
test('successfully creates a checkout session', async () => {
32+
const collection = firestore.collection('customers');
33+
34+
const customer: DocumentData = await waitForDocumentToExistInCollection(
35+
collection,
36+
'email',
37+
user.email
38+
);
39+
40+
const checkoutSessionCollection = collection
41+
.doc(customer.doc.id)
42+
.collection('checkout_sessions');
43+
44+
const checkoutSessionDocument: DocumentReference =
45+
await checkoutSessionCollection.add({
46+
success_url: 'http://test.com/success',
47+
cancel_url: 'http://test.com/cancel',
48+
line_items: [
49+
{
50+
price: price.id,
51+
quantity: 1,
52+
},
53+
],
54+
});
55+
56+
const customerDoc = await waitForDocumentToExistWithField(
57+
checkoutSessionDocument,
58+
'created'
59+
);
60+
61+
const { client, success_url } = customerDoc.data();
62+
63+
expect(client).toBe('web');
64+
expect(success_url).toBe('http://test.com/success');
65+
});
66+
67+
test.skip('throws an error when success_url has not been provided', async () => {});
68+
69+
test.skip('throws an error when cancel_url has not been provided', async () => {});
70+
test.skip('throws an error when a line items parameter has not been provided', async () => {});
71+
test.skip('throws an error when a subscription data array parameter has not been provided', async () => {});
72+
});
73+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as admin from 'firebase-admin';
2+
import { DocumentData } from '@google-cloud/firestore';
3+
4+
import setupEmulator from './helpers/setupEmulator';
5+
import { UserRecord } from 'firebase-functions/v1/auth';
6+
import {
7+
createFirebaseUser,
8+
waitForDocumentToExistInCollection,
9+
} from './helpers/utils';
10+
11+
admin.initializeApp({ projectId: 'demo-project' });
12+
setupEmulator();
13+
14+
const firestore = admin.firestore();
15+
16+
describe('createCustomer', () => {
17+
let user: UserRecord;
18+
beforeEach(async () => {
19+
user = await createFirebaseUser();
20+
});
21+
22+
test('successfully creates a new customers', async () => {
23+
const collection = firestore.collection('customers');
24+
25+
const customer: DocumentData = await waitForDocumentToExistInCollection(
26+
collection,
27+
'email',
28+
user.email
29+
);
30+
31+
const doc = collection.doc(customer.doc.id);
32+
33+
expect(doc.id).toBeDefined();
34+
});
35+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as admin from 'firebase-admin';
2+
import { DocumentData } from '@google-cloud/firestore';
3+
import functions from 'firebase-functions-test';
4+
import * as cloudFunctions from '../src';
5+
import setupEmulator from './helpers/setupEmulator';
6+
7+
import {
8+
createFirebaseUser,
9+
waitForDocumentToExistInCollection,
10+
waitForDocumentToExistWithField,
11+
} from './helpers/utils';
12+
import { UserRecord } from 'firebase-functions/v1/auth';
13+
14+
const testEnv = functions({ projectId: 'demo-project' });
15+
const createPortalLink = testEnv.wrap(cloudFunctions.createPortalLink);
16+
setupEmulator();
17+
18+
const firestore = admin.firestore();
19+
20+
function request(uid: string, returnUrl: string) {
21+
return createPortalLink(
22+
{ returnUrl },
23+
{
24+
auth: {
25+
uid,
26+
token: 'test',
27+
},
28+
}
29+
);
30+
}
31+
32+
describe('createPortalLink', () => {
33+
let user: UserRecord;
34+
beforeEach(async () => {
35+
user = await createFirebaseUser();
36+
});
37+
38+
afterEach(async () => {
39+
await admin.auth().deleteUser(user.uid);
40+
});
41+
42+
test('successfully creates a new portal link', async () => {
43+
const collection = firestore.collection('customers');
44+
45+
const customer: DocumentData = await waitForDocumentToExistInCollection(
46+
collection,
47+
'email',
48+
user.email
49+
);
50+
51+
const doc = collection.doc(customer.doc.id);
52+
const customerDoc = await waitForDocumentToExistWithField(doc, 'stripeId');
53+
54+
const returnUrl = 'http://test.com';
55+
const result = await request(customerDoc.id, returnUrl);
56+
57+
expect(result.object).toBe('billing_portal.session');
58+
expect(result.customer).toBe(customerDoc.data().stripeId);
59+
expect(result.livemode).toBe(false);
60+
expect(result.return_url).toBe(returnUrl);
61+
expect(result.url).toBeDefined();
62+
});
63+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as admin from 'firebase-admin';
2+
import { DocumentData } from '@google-cloud/firestore';
3+
import setupEmulator from './helpers/setupEmulator';
4+
import { findCustomer } from './helpers/stripeApi/customers';
5+
import {
6+
repeat,
7+
waitForDocumentToExistWithField,
8+
waitForDocumentToExistInCollection,
9+
createFirebaseUser,
10+
} from './helpers/utils';
11+
import { UserRecord } from 'firebase-functions/v1/auth';
12+
13+
admin.initializeApp({ projectId: 'demo-project' });
14+
setupEmulator();
15+
16+
const firestore = admin.firestore();
17+
18+
describe('customerDataDeleted', () => {
19+
let user: UserRecord;
20+
beforeEach(async () => {
21+
user = await createFirebaseUser();
22+
});
23+
24+
test('successfully deletes a stripe customer', async () => {
25+
const collection = firestore.collection('customers');
26+
27+
const customer: DocumentData = await waitForDocumentToExistInCollection(
28+
collection,
29+
'email',
30+
user.email
31+
);
32+
33+
const doc = collection.doc(customer.doc.id);
34+
const userDoc = await waitForDocumentToExistWithField(doc, 'stripeId');
35+
36+
await admin.auth().deleteUser(customer.doc.id);
37+
38+
const check = ($) => $?.deleted;
39+
const toRun = () => findCustomer(userDoc.data().stripeId);
40+
await repeat(toRun, check, 5, 2000);
41+
});
42+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"functions": {
3+
"source": "functions"
4+
},
5+
"emulators": {
6+
"functions": {
7+
"port": 5001
8+
},
9+
"firestore": {
10+
"port": 8080
11+
},
12+
"auth": {
13+
"port": 9099
14+
},
15+
"ui": {
16+
"enabled": true
17+
}
18+
},
19+
"firestore": {
20+
"rules": "firestore.rules",
21+
"indexes": "firestore.indexes.json"
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
rules_version = '2';
2+
service cloud.firestore {
3+
4+
match /databases/{database}/documents {
5+
6+
match /products/{id} {
7+
allow read, write;
8+
}
9+
10+
match /checkouts/{id} {
11+
allow read, write;
12+
}
13+
14+
match /customers/{id} {
15+
allow read, write;
16+
}
17+
18+
}
19+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as admin from 'firebase-admin';
2+
import { DocumentData } from '@google-cloud/firestore';
3+
import { Product } from '../src/interfaces';
4+
import setupEmulator from './helpers/setupEmulator';
5+
6+
import { createRandomProduct, updateProduct } from './helpers/setupProducts';
7+
import {
8+
waitForDocumentToExistInCollection,
9+
waitForDocumentUpdate,
10+
} from './helpers/utils';
11+
12+
admin.initializeApp({ projectId: 'demo-project' });
13+
setupEmulator();
14+
15+
const firestore = admin.firestore();
16+
17+
describe('webhook events', () => {
18+
describe('products', () => {
19+
let product: Product;
20+
21+
beforeEach(async () => {
22+
product = await createRandomProduct();
23+
});
24+
test('successfully creates a new product', async () => {
25+
const collection = firestore.collection('products');
26+
const productDoc: DocumentData = await waitForDocumentToExistInCollection(
27+
collection,
28+
'name',
29+
product.name
30+
);
31+
32+
expect(productDoc.doc.data().name).toBe(product.name);
33+
});
34+
35+
test('successfully updates an existing product', async () => {
36+
const updatedProduct: Product = await updateProduct(product.id, {
37+
name: `updated_${product.name}`,
38+
});
39+
40+
const doc = firestore.collection('products').doc(product.id);
41+
42+
const updated = await waitForDocumentUpdate(
43+
doc,
44+
'name',
45+
`updated_${product.name}`
46+
);
47+
48+
expect(updated.data().name).toBe(updatedProduct.name);
49+
});
50+
});
51+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default () => {
2+
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080';
3+
process.env.FIREBASE_FIRESTORE_EMULATOR_ADDRESS = 'localhost:8080';
4+
process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099';
5+
};

0 commit comments

Comments
 (0)