Restructure the `is-paid-org` guard to be Angular 17+ compliant (#9598)
* Document that `is-paid-org` guard in code * Remove unused `MessagingService` dependency * Make assertions about the way the is-paid-org guard should behave * Restructure the `is-paid-org` guard to be Angular 17+ compliant * Random commit to get the build job moving * Undo previous commit
This commit is contained in:
parent
432a4ddd17
commit
c7777c38ef
|
@ -0,0 +1,115 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { provideRouter } from "@angular/router";
|
||||
import { RouterTestingHarness } from "@angular/router/testing";
|
||||
import { MockProxy, any, mock } from "jest-mock-extended";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { isPaidOrgGuard } from "./is-paid-org.guard";
|
||||
|
||||
@Component({
|
||||
template: "<h1>This is the home screen!</h1>",
|
||||
})
|
||||
export class HomescreenComponent {}
|
||||
|
||||
@Component({
|
||||
template: "<h1>This component can only be accessed by a paid organization!</h1>",
|
||||
})
|
||||
export class PaidOrganizationOnlyComponent {}
|
||||
|
||||
@Component({
|
||||
template: "<h1>This is the organization upgrade screen!</h1>",
|
||||
})
|
||||
export class OrganizationUpgradeScreen {}
|
||||
|
||||
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
Object.assign(
|
||||
new Organization(),
|
||||
{
|
||||
id: "myOrgId",
|
||||
enabled: true,
|
||||
type: OrganizationUserType.Admin,
|
||||
},
|
||||
props,
|
||||
);
|
||||
|
||||
describe("Is Paid Org Guard", () => {
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let routerHarness: RouterTestingHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
organizationService = mock<OrganizationService>();
|
||||
dialogService = mock<DialogService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: OrganizationService, useValue: organizationService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
provideRouter([
|
||||
{
|
||||
path: "",
|
||||
component: HomescreenComponent,
|
||||
},
|
||||
{
|
||||
path: "organizations/:organizationId/paidOrganizationsOnly",
|
||||
component: PaidOrganizationOnlyComponent,
|
||||
canActivate: [isPaidOrgGuard()],
|
||||
},
|
||||
{
|
||||
path: "organizations/:organizationId/billing/subscription",
|
||||
component: OrganizationUpgradeScreen,
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
routerHarness = await RouterTestingHarness.create();
|
||||
});
|
||||
|
||||
it("redirects to `/` if the organization id provided is not found", async () => {
|
||||
const org = orgFactory();
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(null);
|
||||
await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`);
|
||||
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
|
||||
"This is the home screen!",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a dialog to users of a free organization and does not proceed with navigation", async () => {
|
||||
// `useTotp` is the current indicator of a free org, it is the baseline
|
||||
// feature offered above the free organization level.
|
||||
const org = orgFactory({ type: OrganizationUserType.User, useTotp: false });
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`);
|
||||
expect(dialogService.openSimpleDialog).toHaveBeenCalled();
|
||||
expect(
|
||||
routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "",
|
||||
).not.toBe("This component can only be accessed by a paid organization!");
|
||||
});
|
||||
|
||||
it("redirects users with billing access to the billing screen to upgrade", async () => {
|
||||
// `useTotp` is the current indicator of a free org, it is the baseline
|
||||
// feature offered above the free organization level.
|
||||
const org = orgFactory({ type: OrganizationUserType.Owner, useTotp: false });
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
dialogService.openSimpleDialog.calledWith(any()).mockResolvedValue(true);
|
||||
await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`);
|
||||
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
|
||||
"This is the organization upgrade screen!",
|
||||
);
|
||||
});
|
||||
|
||||
it("proceeds with navigation if the organization in question is a paid organization", async () => {
|
||||
const org = orgFactory({ useTotp: true });
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`);
|
||||
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
|
||||
"This component can only be accessed by a paid organization!",
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,32 +1,37 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
||||
import { inject } from "@angular/core";
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivateFn,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from "@angular/router";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class IsPaidOrgGuard implements CanActivate {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private organizationService: OrganizationService,
|
||||
private messagingService: MessagingService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
/**
|
||||
* `CanActivateFn` that checks if the organization matching the id in the URL
|
||||
* parameters is paid or free. If the organization is free instructions are
|
||||
* provided on how to upgrade a free organization, and the user is redirected
|
||||
* if they have access to upgrade the organization. If the organization is
|
||||
* paid routing proceeds."
|
||||
*/
|
||||
export function isPaidOrgGuard(): CanActivateFn {
|
||||
return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
const router = inject(Router);
|
||||
const organizationService = inject(OrganizationService);
|
||||
const dialogService = inject(DialogService);
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const org = await this.organizationService.get(route.params.organizationId);
|
||||
const org = await organizationService.get(route.params.organizationId);
|
||||
|
||||
if (org == null) {
|
||||
return this.router.createUrlTree(["/"]);
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
if (org.isFreeOrg) {
|
||||
// Users without billing permission can't access billing
|
||||
if (!org.canEditSubscription) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
await dialogService.openSimpleDialog({
|
||||
title: { key: "upgradeOrganizationCloseSecurityGaps" },
|
||||
content: { key: "upgradeOrganizationCloseSecurityGapsDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
|
@ -35,7 +40,7 @@ export class IsPaidOrgGuard implements CanActivate {
|
|||
});
|
||||
return false;
|
||||
} else {
|
||||
const upgradeConfirmed = await this.dialogService.openSimpleDialog({
|
||||
const upgradeConfirmed = await dialogService.openSimpleDialog({
|
||||
title: { key: "upgradeOrganizationCloseSecurityGaps" },
|
||||
content: { key: "upgradeOrganizationCloseSecurityGapsDesc" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
|
@ -43,7 +48,7 @@ export class IsPaidOrgGuard implements CanActivate {
|
|||
icon: "bwi-arrow-circle-up",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await this.router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
await router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
queryParams: { upgrade: true },
|
||||
});
|
||||
}
|
||||
|
@ -51,5 +56,5 @@ export class IsPaidOrgGuard implements CanActivate {
|
|||
}
|
||||
|
||||
return !org.isFreeOrg;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { InactiveTwoFactorReportComponent } from "../../../admin-console/organiz
|
|||
import { ReusedPasswordsReportComponent } from "../../../admin-console/organizations/tools/reused-passwords-report.component";
|
||||
import { UnsecuredWebsitesReportComponent } from "../../../admin-console/organizations/tools/unsecured-websites-report.component";
|
||||
import { WeakPasswordsReportComponent } from "../../../admin-console/organizations/tools/weak-passwords-report.component";
|
||||
import { IsPaidOrgGuard } from "../guards/is-paid-org.guard";
|
||||
import { isPaidOrgGuard } from "../guards/is-paid-org.guard";
|
||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "../guards/org-redirect.guard";
|
||||
import { EventsComponent } from "../manage/events.component";
|
||||
|
@ -42,7 +42,7 @@ const routes: Routes = [
|
|||
data: {
|
||||
titleId: "exposedPasswordsReport",
|
||||
},
|
||||
canActivate: [IsPaidOrgGuard],
|
||||
canActivate: [isPaidOrgGuard()],
|
||||
},
|
||||
{
|
||||
path: "inactive-two-factor-report",
|
||||
|
@ -50,7 +50,7 @@ const routes: Routes = [
|
|||
data: {
|
||||
titleId: "inactive2faReport",
|
||||
},
|
||||
canActivate: [IsPaidOrgGuard],
|
||||
canActivate: [isPaidOrgGuard()],
|
||||
},
|
||||
{
|
||||
path: "reused-passwords-report",
|
||||
|
@ -58,7 +58,7 @@ const routes: Routes = [
|
|||
data: {
|
||||
titleId: "reusedPasswordsReport",
|
||||
},
|
||||
canActivate: [IsPaidOrgGuard],
|
||||
canActivate: [isPaidOrgGuard()],
|
||||
},
|
||||
{
|
||||
path: "unsecured-websites-report",
|
||||
|
@ -66,7 +66,7 @@ const routes: Routes = [
|
|||
data: {
|
||||
titleId: "unsecuredWebsitesReport",
|
||||
},
|
||||
canActivate: [IsPaidOrgGuard],
|
||||
canActivate: [isPaidOrgGuard()],
|
||||
},
|
||||
{
|
||||
path: "weak-passwords-report",
|
||||
|
@ -74,7 +74,7 @@ const routes: Routes = [
|
|||
data: {
|
||||
titleId: "weakPasswordsReport",
|
||||
},
|
||||
canActivate: [IsPaidOrgGuard],
|
||||
canActivate: [isPaidOrgGuard()],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue