1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 05:53:42 +00:00

test(billing): update premium org upgrade payment component tests

This commit is contained in:
Stephon Brown
2026-02-04 18:38:22 -05:00
parent ac5c73f704
commit b8f4292dc4

View File

@@ -10,8 +10,9 @@ import { FormControl, FormGroup, Validators } from "@angular/forms";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
import {
BusinessSubscriptionPricingTier,
@@ -28,9 +29,10 @@ import { CartSummaryComponent } from "@bitwarden/pricing";
import { AccountBillingClient } from "../../../clients/account-billing.client";
import { PreviewInvoiceClient } from "../../../clients/preview-invoice.client";
import { SubscriberBillingClient } from "../../../clients/subscriber-billing.client";
import {
EnterBillingAddressComponent,
EnterPaymentMethodComponent,
DisplayPaymentMethodComponent,
} from "../../../payment/components";
import {
@@ -54,40 +56,19 @@ class MockCartSummaryComponent {
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-enter-payment-method",
template: `<h1>Mock Enter Payment Method</h1>`,
selector: "app-display-payment-method",
template: `<h1>Mock Display Payment Method</h1>`,
providers: [
{
provide: EnterPaymentMethodComponent,
useClass: MockEnterPaymentMethodComponent,
provide: DisplayPaymentMethodComponent,
useClass: MockDisplayPaymentMethodComponent,
},
],
})
class MockEnterPaymentMethodComponent {
readonly group = input.required<any>();
readonly showBankAccount = input(true);
readonly showPayPal = input(true);
readonly showAccountCredit = input(false);
readonly hasEnoughAccountCredit = input(true);
readonly includeBillingAddress = input(false);
tokenize = jest.fn().mockResolvedValue({ type: "card", token: "mock-token" });
validate = jest.fn().mockReturnValue(true);
static getFormGroup = () =>
new FormGroup({
type: new FormControl<string>("card", { nonNullable: true }),
bankAccount: new FormGroup({
routingNumber: new FormControl<string>("", { nonNullable: true }),
accountNumber: new FormControl<string>("", { nonNullable: true }),
accountHolderName: new FormControl<string>("", { nonNullable: true }),
accountHolderType: new FormControl<string>("", { nonNullable: true }),
}),
billingAddress: new FormGroup({
country: new FormControl<string>("", { nonNullable: true }),
postalCode: new FormControl<string>("", { nonNullable: true }),
}),
});
class MockDisplayPaymentMethodComponent {
readonly subscriber = input.required<any>();
readonly paymentMethod = input<any>();
readonly hideHeader = input<boolean>();
}
@Component({
@@ -148,6 +129,9 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
const mockPreviewInvoiceClient = mock<PreviewInvoiceClient>();
const mockLogService = mock<LogService>();
const mockOrganizationService = mock<OrganizationService>();
const mockSubscriberBillingClient = mock<SubscriberBillingClient>();
const mockApiService = mock<ApiService>();
const mockAccountService = mock<AccountService>();
const mockI18nService = { t: jest.fn((key: string, ...params: any[]) => key) };
const mockAccount = { id: "user-id", email: "test@bitwarden.com" } as Account;
@@ -191,6 +175,13 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
newPlanProratedMonths: 1,
});
mockOrganizationService.organizations$.mockReturnValue(of([]));
mockAccountService.activeAccount$ = of(mockAccount);
mockSubscriberBillingClient.getPaymentMethod.mockResolvedValue({
type: "card",
brand: "visa",
last4: "4242",
expiration: "12/2025",
});
mockSubscriptionPricingService.getBusinessSubscriptionPricingTiers$.mockReturnValue(
of([mockTeamsPlan]),
@@ -212,6 +203,9 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
{ provide: I18nService, useValue: mockI18nService },
{ provide: AccountBillingClient, useValue: mockAccountBillingClient },
{ provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient },
{ provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient },
{ provide: AccountService, useValue: mockAccountService },
{ provide: ApiService, useValue: mockApiService },
{
provide: KeyService,
useValue: {
@@ -230,14 +224,14 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
add: {
imports: [
MockEnterBillingAddressComponent,
MockEnterPaymentMethodComponent,
MockDisplayPaymentMethodComponent,
MockCartSummaryComponent,
],
},
remove: {
imports: [
EnterBillingAddressComponent,
EnterPaymentMethodComponent,
DisplayPaymentMethodComponent,
CartSummaryComponent,
],
},
@@ -304,8 +298,7 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
it("should successfully upgrade to organization", async () => {
const completeSpy = jest.spyOn(component["complete"], "emit");
// Mock isFormValid and processUpgrade to bypass form validation
jest.spyOn(component as any, "isFormValid").mockReturnValue(true);
// Mock processUpgrade to bypass form validation
jest.spyOn(component as any, "processUpgrade").mockResolvedValue({
status: PremiumOrgUpgradePaymentStatus.UpgradedToTeams,
organizationId: null,
@@ -313,19 +306,6 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
component["formGroup"].setValue({
organizationName: "My New Org",
paymentForm: {
type: "card",
bankAccount: {
routingNumber: "",
accountNumber: "",
accountHolderName: "",
accountHolderType: "",
},
billingAddress: {
country: "",
postalCode: "",
},
},
billingAddress: {
country: "US",
postalCode: "90210",
@@ -350,8 +330,6 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
});
it("should show an error toast if upgrade fails", async () => {
// Mock isFormValid to return true
jest.spyOn(component as any, "isFormValid").mockReturnValue(true);
// Mock processUpgrade to throw an error
jest
.spyOn(component as any, "processUpgrade")
@@ -359,19 +337,6 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
component["formGroup"].setValue({
organizationName: "My New Org",
paymentForm: {
type: "card",
bankAccount: {
routingNumber: "",
accountNumber: "",
accountHolderName: "",
accountHolderType: "",
},
billingAddress: {
country: "",
postalCode: "",
},
},
billingAddress: {
country: "US",
postalCode: "90210",
@@ -428,7 +393,8 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
},
});
tick(1000);
// Advance time to allow any async operations to complete
tick(1500);
fixture.detectChanges();
const estimatedInvoice = component["estimatedInvoice"]();
@@ -447,23 +413,6 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
component["formGroup"].patchValue({ organizationName: "My Organization" });
expect(component["formGroup"].get("organizationName")?.valid).toBe(true);
});
it("should return false when payment component validation fails", () => {
component["formGroup"].patchValue({
organizationName: "Test Org",
billingAddress: {
country: "US",
postalCode: "12345",
},
});
const mockPaymentComponent = {
validate: jest.fn().mockReturnValue(false),
} as any;
jest.spyOn(component, "paymentComponent").mockReturnValue(mockPaymentComponent);
expect(component["isFormValid"]()).toBe(false);
});
});
describe("Cart Calculation", () => {
@@ -516,6 +465,16 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
});
describe("processUpgrade", () => {
beforeEach(() => {
// Set paymentMethod signal for these tests
component["paymentMethod"].set({
type: "card",
brand: "visa",
last4: "4242",
expiration: "12/2025",
});
});
it("should throw error when billing address is incomplete", async () => {
component["formGroup"].patchValue({
organizationName: "Test Org",
@@ -525,11 +484,6 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
},
});
const mockPaymentComponent = {
tokenize: jest.fn().mockResolvedValue({ type: "card", token: "mock-token" }),
} as any;
jest.spyOn(component, "paymentComponent").mockReturnValue(mockPaymentComponent);
await expect(component["processUpgrade"]()).rejects.toThrow("Billing address is incomplete");
});
@@ -542,30 +496,8 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
},
});
const mockPaymentComponent = {
tokenize: jest.fn().mockResolvedValue({ type: "card", token: "mock-token" }),
} as any;
jest.spyOn(component, "paymentComponent").mockReturnValue(mockPaymentComponent);
await expect(component["processUpgrade"]()).rejects.toThrow("Organization name is required");
});
it("should throw error when payment method tokenization fails", async () => {
component["formGroup"].patchValue({
organizationName: "Test Org",
billingAddress: {
country: "US",
postalCode: "12345",
},
});
const mockPaymentComponent = {
tokenize: jest.fn().mockResolvedValue(null),
} as any;
jest.spyOn(component, "paymentComponent").mockReturnValue(mockPaymentComponent);
await expect(component["processUpgrade"]()).rejects.toThrow("Payment method is required");
});
});
describe("Plan Membership Messages", () => {
@@ -606,9 +538,21 @@ describe("PremiumOrgUpgradePaymentComponent", () => {
describe("Error Handling", () => {
it("should log error and continue when submit fails", async () => {
jest.spyOn(component as any, "isFormValid").mockReturnValue(true);
jest.spyOn(component as any, "processUpgrade").mockRejectedValue(new Error("Network error"));
component["formGroup"].setValue({
organizationName: "My New Org",
billingAddress: {
country: "US",
postalCode: "90210",
line1: "123 Main St",
line2: "",
city: "Beverly Hills",
state: "CA",
taxId: "",
},
});
await component["submit"]();
expect(mockLogService.error).toHaveBeenCalledWith("Upgrade failed:", expect.any(Error));