Skip to content

test(auth-server): migrate payment unit tests from Mocha to Jest#20204

Merged
vbudhram merged 1 commit intomainfrom
fxa-12617
Mar 24, 2026
Merged

test(auth-server): migrate payment unit tests from Mocha to Jest#20204
vbudhram merged 1 commit intomainfrom
fxa-12617

Conversation

@vbudhram
Copy link
Copy Markdown
Contributor

@vbudhram vbudhram commented Mar 17, 2026

Because

  • Payment tests in test/local/payments/ use Mocha + Chai + proxyquire, which is legacy test infra being phased out in favor of Jest
  • Co-locating tests next to source files (.spec.ts) aligns with the monorepo's preferred testing pattern

This pull request

  • Migrates 8 payment test suites from Mocha/Chai to Jest/expect
  • Converts assert.* / sinon.assert.* to expect().* while preserving sinon for stub verification
  • Replaces proxyquire with jest.mock for module mocking
  • Co-locates new .spec.ts files alongside their source in lib/payments/
  • Adds TypeScript type annotations to test variables

Migration coverage comparison

Test suite Mocha tests Jest tests Mocha asserts Jest asserts Status
stripe 324 320 630 591 ~4 dead-code tests dropped
capability 45 45 87 87 Full parity
stripe-firestore 59 59 169 151 Full parity
subscription-reminders 43 43 149 148 Full parity
stripe-formatter 8 8 55 55 Full parity
currencies 12 12 18 18 Full parity
utils 2 2 3 3 Full parity
configuration/manager 19 19 41 33 Full parity
Total 512 508 1152 1086

Notes on deltas:

  • Test count (-4): 2 were dead code in Mocha (nested it() inside it(), silently skipped). 2 are dynamic test name differences in the grep count — all functional tests are present.
  • Assertion count (-66): Primarily from replacing redundant sinon.assert.calledOnceWithExactly stub-call verifications with return-value assertions, removing duplicate assertions, and converting assert.fail() sentinels to throw new Error().
  • No coverage regressions. All production code paths tested in Mocha are tested in Jest.

Issue

Closes: https://mozilla-hub.atlassian.net/browse/FXA-12617

Checklist

  • My commit is GPG signed
  • Tests pass locally (if applicable)
  • Documentation updated (if applicable)
  • RTL rendering verified (if UI changed)

@vbudhram vbudhram requested a review from a team as a code owner March 17, 2026 20:28
@vbudhram vbudhram self-assigned this Mar 18, 2026
Copilot AI review requested due to automatic review settings March 23, 2026 14:52
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates several auth-server payments tests from the legacy Mocha suite (test/local/...) into Jest (*.spec.ts) so they run under the repo’s Jest unit and integration configurations.

Changes:

  • Adds Jest integration coverage for PaymentConfigManager under test/remote/payments/....
  • Adds Jest unit specs for payments helpers/modules (currencies, utils, stripe formatter, stripe firestore, subscription reminders, capability service) under lib/payments/....
  • Ports existing fixtures/mocks usage from test/local/... into the Jest test environment.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/fxa-auth-server/test/remote/payments/configuration/manager.spec.ts New Jest integration spec for PaymentConfigManager using Firestore + typedi wiring.
packages/fxa-auth-server/lib/payments/utils.spec.ts New Jest unit spec for roundTime and sortClientCapabilities.
packages/fxa-auth-server/lib/payments/subscription-reminders.spec.ts New Jest unit spec for subscription reminder logic and email-sending behavior.
packages/fxa-auth-server/lib/payments/stripe-formatter.spec.ts New Jest unit spec for Stripe invoice DTO formatting helpers.
packages/fxa-auth-server/lib/payments/stripe-firestore.spec.ts New Jest unit spec for StripeFirestore behaviors using mocked Firestore/Stripe.
packages/fxa-auth-server/lib/payments/currencies.spec.ts New Jest unit spec for CurrencyHelper constructor validation and PayPal amount formatting.
packages/fxa-auth-server/lib/payments/capability.spec.ts New Jest unit spec for CapabilityService behaviors and integrations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

planConfigDbRef,
100
);
sandbox.reset();
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sandbox.reset() does not restore spies/stubs created with this sandbox, so wrapped methods can leak between tests (e.g., sandbox.spy(ProductConfig, 'validate')). Use sandbox.restore() (or sandbox.restore(); sandbox.resetHistory();) in afterEach to ensure originals are restored and avoid "already wrapped" failures.

Suggested change
sandbox.reset();
sandbox.restore();

Copilot uses AI. Check for mistakes.
Comment on lines +318 to +325
const product = (await paymentConfigManager.allProducts())[0];
delete newPlan.active;

try {
await paymentConfigManager.validatePlanConfig(newPlan, product.id);
throw new Error('should have thrown');
} catch (err: any) {
expect(err.errno).toBe(errors.ERRNO.INTERNAL_VALIDATION_ERROR);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is labeled as validating an invalid product id, but it deletes active on the plan config, so it only exercises schema validation (missing required field) and never reaches the product-existence check in validatePlanConfig. Keep the plan valid and pass a non-existent productConfigId to assert the expected ProductConfig does not exist error.

Suggested change
const product = (await paymentConfigManager.allProducts())[0];
delete newPlan.active;
try {
await paymentConfigManager.validatePlanConfig(newPlan, product.id);
throw new Error('should have thrown');
} catch (err: any) {
expect(err.errno).toBe(errors.ERRNO.INTERNAL_VALIDATION_ERROR);
const invalidProductId = randomUUID();
try {
await paymentConfigManager.validatePlanConfig(newPlan, invalidProductId);
throw new Error('should have thrown');
} catch (err: any) {
expect(err.message).toContain('ProductConfig does not exist');

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +62
it('throws an error if invalid currencyCode', () => {
const invalidCurrency = {
ZZZZZ: ['US', 'CA'],
};
expect(() => {
(CurrencyHelper as any)({ currenciesToCountries: invalidCurrency });
}).toThrow();
});

it('throws an error if invalid countryCode', () => {
const invalidCountry = {
AUD: ['AUS'],
};
expect(() => {
(CurrencyHelper as any)({ currenciesToCountries: invalidCountry });
}).toThrow();
});
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests call CurrencyHelper like a function. Since CurrencyHelper is a class, this throws for the wrong reason ("cannot be invoked without 'new'") and can mask real validation behavior. Instantiate with new CurrencyHelper(...) so the tests assert the intended validation errors.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +90
it('throws an error if currency not in paypal supported, if paypalEnabled', () => {
const currenciesToCountries = {
USD: ['US', 'CA'],
};
expect(() => {
(CurrencyHelper as any)({
currenciesToCountries,
subscriptions: payPalEnabledSubscriptionsConfig,
});
}).toThrow();
});
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PayPal-enabled validation test uses USD, which is in CurrencyHelper.supportedPayPalCurrencies, so new CurrencyHelper(...) should not throw. Once the constructor call is fixed, update this test data to use a currency code that is valid but not PayPal-supported (e.g., JPY) so it exercises the intended branch.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +28
function buildExpectedLineItems(invoice: any) {
return invoice.line_items.map((item: any) => ({
amount: item.amount,
currency: item.currency,
id: item.id,
name: item.name,
period: {
end: item.period.end,
start: item.period.start,
},
}));
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildExpectedLineItems derives the expected value from invoice.line_items (the actual output), so it won't catch incorrect field values coming from the Stripe fixture (only extra/missing fields). Build expected line items from the source fixture invoice data instead to make this test meaningful.

Copilot uses AI. Check for mistakes.
Comment on lines +1467 to +1474
mockChurnInterventionService.determineStaySubscribedEligibility =
sandbox.fake.resolves({
isEligibile: true,
cmsChurnInterventionEntry: {
ctaMessage: mockCtaMessage,
ctaButtonUrl: mockProductPageUrl,
},
});
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

determineStaySubscribedEligibility is mocked to return isEligibile, but SubscriptionReminders destructures isEligible. This typo makes showChurn always falsy in the test and can hide eligibility-related behavior. Rename the field to isEligible and consider asserting showChurn in the mailer payload.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +56
stripeFirestore = new StripeFirestore(
firestore,
customerCollectionDbRef,
stripe
);
});
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StripeFirestore's constructor requires a prefix: string (see lib/payments/stripe-firestore.ts), but the test instantiates it with only 3 arguments. This should be a TypeScript compile error and/or can change runtime behavior (prefix becomes undefined). Pass an explicit prefix in these constructor calls (and keep it consistent across the file).

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +63
const stripeFirestore = new StripeFirestore(
firestore,
customerCollectionDbRef,
stripe
);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same constructor arity issue here: new StripeFirestore(firestore, customerCollectionDbRef, stripe) is missing the required prefix argument. Provide the prefix so the test compiles and matches production usage.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@StaberindeZA StaberindeZA left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. Did a couple of spot checks.

@vbudhram vbudhram merged commit b229b41 into main Mar 24, 2026
21 checks passed
@vbudhram vbudhram deleted the fxa-12617 branch March 24, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants