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
119 changes: 118 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ parameters:
untrusted-relier-domain:
type: string
default: ''
payments-next-domain:
type: string
default: ''

orbs:
browser-tools: circleci/browser-tools@1.2.3
Expand Down Expand Up @@ -351,6 +354,9 @@ commands:
parameters:
project:
type: string
test_dir:
type: string
default: tests
grep_invert:
type: string
default: ''
Expand All @@ -376,7 +382,7 @@ commands:
fi
echo "targeting project << parameters.project >> $GREP $GREP_INVERT"
npx nx build fxa-auth-client
cd packages/functional-tests/tests
cd packages/functional-tests/<< parameters.test_dir >>
TEST_FILES=$(circleci tests glob "./**/*.spec.ts")
cd ..

Expand All @@ -389,6 +395,7 @@ commands:
NODE_OPTIONS: --dns-result-order=ipv4first
ACCOUNTS_DOMAIN: << pipeline.parameters.accounts-domain >>
PAYMENTS_DOMAIN: << pipeline.parameters.payments-domain >>
PAYMENTS_NEXT_DOMAIN: << pipeline.parameters.payments-next-domain >>
ACCOUNTS_API_DOMAIN: << pipeline.parameters.accounts-api-domain >>
RELIER_DOMAIN: << pipeline.parameters.relier-domain >>
UNTRUSTED_RELIER_DOMAIN: << pipeline.parameters.untrusted-relier-domain >>
Expand Down Expand Up @@ -468,6 +475,9 @@ commands:
- packages/**/locales
- packages/**/emails/css
- configs/gql/allowlist
- apps/**/public/locales
- apps/**/.next
- libs/**/glean/__generated__

restore-workspace:
steps:
Expand Down Expand Up @@ -876,6 +886,85 @@ jobs:
workflow: << parameters.workflow >>
test_suite: e2e

# Runs payments-next functional tests using playwright.
# For local: uses functional-test-executor with full infra.
# For stage/production: uses smoke-test-executor.
playwright-payments-tests:
parameters:
parallelism:
type: integer
default: 1
project:
type: string
default: local-payments-next
grep:
type: string
default: ''
workflow:
type: string
fail_fast:
type: boolean
default: true
executor: functional-test-executor
parallelism: << parameters.parallelism >>
steps:
- git-checkout
- restore-workspace
- gcp-cli/setup
- run:
name: Add localhost
command: |
sudo tee -a /etc/hosts \<<<'127.0.0.1 localhost'
sudo cat /etc/hosts
- wait-for-infrastructure
- run:
name: Start services for payments playwright tests
command: ./packages/functional-tests/scripts/start-payments-services.sh
environment:
NODE_ENV: test
MYSQL_CONFIG__HOST: 127.0.0.1
no_output_timeout: 10m
- run-playwright-tests:
project: << parameters.project >>
test_dir: tests-payments-next
fail_fast: << parameters.fail_fast >>
- store-artifacts
- upload_to_gcs:
workflow: << parameters.workflow >>
test_suite: e2e-payments

# Runs payments-next smoke tests against remote targets (stage/production).
payments-smoke-tests:
parameters:
parallelism:
type: integer
default: 4
project:
type: string
default: stage-payments-next
grep:
type: string
default: '#smoke'
workflow:
type: string
fail_fast:
type: boolean
default: true
executor: smoke-test-executor
parallelism: << parameters.parallelism >>
steps:
- git-checkout
- gcp-cli/setup
- provision
- run-playwright-tests:
project: << parameters.project >>
test_dir: tests-payments-next
fail_fast: << parameters.fail_fast >>
- store-artifacts
- upload_to_gcs:
workflow: << parameters.workflow >>
test_suite: e2e-payments

update-yarn-cache:
executor: default-executor
resource_class: medium+
Expand Down Expand Up @@ -1018,6 +1107,16 @@ workflows:
only: /.*/
tags:
only: /.*/
- payments-smoke-tests:
name: Payments Smoke Tests (production)
workflow: production_smoke_tests
project: production-payments-next
grep: '#smoke'
filters:
branches:
only: /.*/
tags:
only: /.*/

# Triggered remotely. See .circleci/README.md
stage_smoke_tests:
Expand All @@ -1033,6 +1132,17 @@ workflows:
tags:
only: /.*/
fail_fast: false
- payments-smoke-tests:
name: Payments Smoke Tests (stage)
workflow: stage_smoke_tests
project: stage-payments-next
grep: '#smoke'
filters:
branches:
only: /.*/
tags:
only: /.*/
fail_fast: false

deploy_fxa_image:
# This workflow can be triggered after a PR lands on main. It requires approval.
Expand Down Expand Up @@ -1306,6 +1416,12 @@ workflows:
requires:
- Build (nightly)
fail_fast: false
- playwright-payments-tests:
name: Payments Functional Tests - Playwright (nightly)
workflow: nightly
requires:
- Build (nightly)
fail_fast: false
- on-complete:
name: Tests Complete (nightly)
stage: Tests (nightly)
Expand All @@ -1320,6 +1436,7 @@ workflows:
- Integration Test - Servers - Auth Scripts (nightly)
- Integration Test - Libraries (nightly)
- Firefox Functional Tests - Playwright (nightly)
- Payments Functional Tests - Playwright (nightly)
- create-fxa-image:
name: Create FxA Image (nightly)
requires:
Expand Down
8 changes: 2 additions & 6 deletions libs/payments/metrics/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@
"inputs": [
"{workspaceRoot}/libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml"
],
"outputs": [
"{projectRoot}/src/lib/glean/__generated__/server_events.ts"
],
"outputs": ["{projectRoot}/src/lib/glean/__generated__/server_events.ts"],
"command": "yarn glean translate libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml -f typescript_server -o libs/payments/metrics/src/lib/glean/__generated__"
},
"glean-lint": {
Expand All @@ -51,9 +49,7 @@
"inputs": [
"{workspaceRoot}/libs/shared/metrics/glean/src/registry/subplat-frontend-metrics.yaml"
],
"outputs": [
"{projectRoot}/src/lib/glean/__generated__/subscriptions.ts"
],
"outputs": ["{projectRoot}/src/lib/glean/__generated__/subscriptions.ts"],
"command": "yarn glean translate libs/shared/metrics/glean/src/registry/subplat-frontend-metrics.yaml -f typescript -o libs/payments/metrics/src/lib/glean/__generated__"
},
"glean-lint-frontend": {
Expand Down
25 changes: 25 additions & 0 deletions packages/functional-tests/lib/stripe-test-cards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* Stripe test card numbers for use in functional tests.
* See: https://docs.stripe.com/testing#cards
*/

export const StripeTestCards = {
SUCCESS: '4242424242424242',
THREE_DS_REQUIRED: '4000002760003184',
DECLINED: '4000000000000002',
INSUFFICIENT_FUNDS: '4000000000009995',
EXPIRED: '4000000000000069',
} as const;

export type StripeTestCard =
(typeof StripeTestCards)[keyof typeof StripeTestCards];

export const TestCardDefaults = {
EXP_MONTH: 12,
EXP_YEAR: new Date().getFullYear() + 3,
CVC: '123',
} as const;
86 changes: 86 additions & 0 deletions packages/functional-tests/lib/sub-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Stripe from 'stripe';
import {
StripeTestCards,
StripeTestCard,
TestCardDefaults,
} from './stripe-test-cards';

function getStripeClient(): Stripe {
const key = process.env.STRIPE_API_KEY;
if (!key) {
throw new Error(
'STRIPE_API_KEY environment variable is required for subscription helpers'
);
}
return new Stripe(key, {
apiVersion: '2024-11-20.acacia',
});
}

/**
* Create a Stripe customer in test mode.
*/
export async function createStripeTestCustomer(
email: string
): Promise<Stripe.Customer> {
const stripe = getStripeClient();
return stripe.customers.create({ email });
}

/**
* Attach a test card payment method to a customer.
*/
export async function attachPaymentMethod(
customerId: string,
card: StripeTestCard = StripeTestCards.SUCCESS
): Promise<Stripe.PaymentMethod> {
const stripe = getStripeClient();
const paymentMethod = await stripe.paymentMethods.create({
type: 'card',
card: {
number: card,
exp_month: TestCardDefaults.EXP_MONTH,
exp_year: TestCardDefaults.EXP_YEAR,
cvc: TestCardDefaults.CVC,
},
});
await stripe.paymentMethods.attach(paymentMethod.id, {
customer: customerId,
});
await stripe.customers.update(customerId, {
invoice_settings: { default_payment_method: paymentMethod.id },
});
return paymentMethod;
}

/**
* Create a subscription via the Stripe API — for manage/cancel tests.
*/
export async function createTestSubscription(
customerId: string,
priceId: string
): Promise<Stripe.Subscription> {
const stripe = getStripeClient();
return stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
});
}

/**
* Cancel all subscriptions and delete a customer — for teardown.
*/
export async function cleanupStripeCustomer(customerId: string): Promise<void> {
const stripe = getStripeClient();
const subscriptions = await stripe.subscriptions.list({
customer: customerId,
});
for (const sub of subscriptions.data) {
await stripe.subscriptions.cancel(sub.id);
}
await stripe.customers.del(customerId);
}
3 changes: 3 additions & 0 deletions packages/functional-tests/lib/targets/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export abstract class BaseTarget {
readonly authClient: AuthClient;
readonly emailClient: EmailClient;
abstract readonly contentServerUrl: string;
abstract readonly paymentsNextUrl: string;
abstract readonly paymentsTestOfferingId: string;
abstract readonly paymentsTestPriceId: string;
abstract readonly relierUrl: string;
abstract readonly relierClientID: string;
abstract readonly name: TargetName;
Expand Down
3 changes: 3 additions & 0 deletions packages/functional-tests/lib/targets/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class LocalTarget extends BaseTarget {
static readonly target = 'local';
readonly name: TargetName = LocalTarget.target;
readonly contentServerUrl = 'http://localhost:3030';
readonly paymentsNextUrl = 'http://localhost:3035';
readonly paymentsTestOfferingId = '123donepro';
readonly paymentsTestPriceId = 'price_1NSnz3BVqmGyQTMaIkV5wjEc';
readonly relierUrl = 'http://localhost:8080';
readonly relierClientID = RELIER_CLIENT_ID;
readonly rateLimitClient: RateLimitClient;
Expand Down
5 changes: 5 additions & 0 deletions packages/functional-tests/lib/targets/production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { RemoteTarget } from './remote';
const ACCOUNTS_DOMAIN = process.env.ACCOUNTS_DOMAIN || 'accounts.firefox.com';
const ACCOUNTS_API_DOMAIN =
process.env.ACCOUNTS_API_DOMAIN || 'api.accounts.firefox.com';
const PAYMENTS_NEXT_DOMAIN =
process.env.PAYMENTS_NEXT_DOMAIN || 'payments.firefox.com';
const RELIER_DOMAIN =
process.env.RELIER_DOMAIN || 'production-123done.herokuapp.com';
const RELIER_CLIENT_ID = '3c32bf6654542211';
Expand All @@ -16,6 +18,9 @@ export class ProductionTarget extends RemoteTarget {
static readonly target = 'production';
readonly name: TargetName = ProductionTarget.target;
readonly contentServerUrl = `https://${ACCOUNTS_DOMAIN}`;
readonly paymentsNextUrl = `https://${PAYMENTS_NEXT_DOMAIN}`;
readonly paymentsTestOfferingId = 'vpn';
readonly paymentsTestPriceId = '';
readonly relierUrl = `https://${RELIER_DOMAIN}`;
readonly relierClientID = RELIER_CLIENT_ID;

Expand Down
5 changes: 5 additions & 0 deletions packages/functional-tests/lib/targets/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const ACCOUNTS_DOMAIN =
process.env.ACCOUNTS_DOMAIN || 'accounts.stage.mozaws.net';
const ACCOUNTS_API_DOMAIN =
process.env.ACCOUNTS_API_DOMAIN || 'api-accounts.stage.mozaws.net';
const PAYMENTS_NEXT_DOMAIN =
process.env.PAYMENTS_NEXT_DOMAIN || 'payments-next.allizom.org';
const RELIER_DOMAIN =
process.env.RELIER_DOMAIN || 'stage-123done.herokuapp.com';
const RELIER_CLIENT_ID = 'dcdb5ae7add825d2';
Expand All @@ -17,6 +19,9 @@ export class StageTarget extends RemoteTarget {
static readonly target = 'stage';
readonly name: TargetName = StageTarget.target;
readonly contentServerUrl = `https://${ACCOUNTS_DOMAIN}`;
readonly paymentsNextUrl = `https://${PAYMENTS_NEXT_DOMAIN}`;
readonly paymentsTestOfferingId = '123donepro';
readonly paymentsTestPriceId = 'price_1QACG5Kb9q6OnNsLtEpM8dbm';
readonly relierUrl = `https://${RELIER_DOMAIN}`;
readonly relierClientID = RELIER_CLIENT_ID;

Expand Down
Loading