Restructued is-enterprise-org.guard to be a function. (#10134)

Added test cases for IsEnterpriseOrgGuard

Co-authored-by: Tom <ttalty@bitwarden.com>
Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com>
This commit is contained in:
aj-rosado 2024-07-24 10:08:33 +01:00 committed by GitHub
parent 97f44ace66
commit 9a6b37d019
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 159 additions and 32 deletions

View File

@ -0,0 +1,126 @@
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 { ProductTierType } from "@bitwarden/common/billing/enums";
import { DialogService } from "@bitwarden/components";
import { isEnterpriseOrgGuard } from "./is-enterprise-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 enterprise organization!</h1>",
})
export class IsEnterpriseOrganizationComponent {}
@Component({
template: "<h1>This is the organization upgrade screen!</h1>",
})
export class OrganizationUpgradeScreenComponent {}
const orgFactory = (props: Partial<Organization> = {}) =>
Object.assign(
new Organization(),
{
id: "myOrgId",
enabled: true,
type: OrganizationUserType.Admin,
},
props,
);
describe("Is Enterprise 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/enterpriseOrgsOnly",
component: IsEnterpriseOrganizationComponent,
canActivate: [isEnterpriseOrgGuard()],
},
{
path: "organizations/:organizationId/billing/subscription",
component: OrganizationUpgradeScreenComponent,
},
]),
],
});
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}/enterpriseOrgsOnly`);
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
"This is the home screen!",
);
});
it.each([
ProductTierType.Free,
ProductTierType.Families,
ProductTierType.Teams,
ProductTierType.TeamsStarter,
])(
"shows a dialog to users of a not enterprise organization and does not proceed with navigation for productTierType '%s'",
async (productTierType) => {
const org = orgFactory({
type: OrganizationUserType.User,
productTierType: productTierType,
});
organizationService.get.calledWith(org.id).mockResolvedValue(org);
await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`);
expect(dialogService.openSimpleDialog).toHaveBeenCalled();
expect(
routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "",
).not.toBe("This component can only be accessed by a enterprise organization!");
},
);
it("redirects users with billing access to the billing screen to upgrade", async () => {
const org = orgFactory({
type: OrganizationUserType.Owner,
productTierType: ProductTierType.Teams,
});
organizationService.get.calledWith(org.id).mockResolvedValue(org);
dialogService.openSimpleDialog.calledWith(any()).mockResolvedValue(true);
await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`);
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 enterprise organization", async () => {
const org = orgFactory({ productTierType: ProductTierType.Enterprise });
organizationService.get.calledWith(org.id).mockResolvedValue(org);
await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`);
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
"This component can only be accessed by a enterprise organization!",
);
});
});

View File

@ -1,44 +1,45 @@
import { Injectable } from "@angular/core"; import { inject } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; import {
import { firstValueFrom } from "rxjs"; ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from "@angular/router";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProductTierType } from "@bitwarden/common/billing/enums"; import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@Injectable({ /**
providedIn: "root", * `CanActivateFn` that checks if the organization matching the id in the URL
}) * parameters is of enterprise type. If the organization is not enterprise instructions are
export class IsEnterpriseOrgGuard implements CanActivate { * provided on how to upgrade into an enterprise organization, and the user is redirected
constructor( * if they have access to upgrade the organization. If the organization is
private router: Router, * enterprise routing proceeds."
private organizationService: OrganizationService, */
private dialogService: DialogService, export function isEnterpriseOrgGuard(): CanActivateFn {
private configService: ConfigService, 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 organizationService.get(route.params.organizationId);
const isMemberAccessReportEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.MemberAccessReport),
);
// TODO: Remove on "MemberAccessReport" feature flag cleanup
if (!isMemberAccessReportEnabled) {
return this.router.createUrlTree(["/"]);
}
const org = await this.organizationService.get(route.params.organizationId);
if (org == null) { if (org == null) {
return this.router.createUrlTree(["/"]); return router.createUrlTree(["/"]);
}
// TODO: Remove on "MemberAccessReport" feature flag cleanup
if (!canAccessFeature(FeatureFlag.MemberAccessReport)) {
return router.createUrlTree(["/"]);
} }
if (org.productTierType != ProductTierType.Enterprise) { if (org.productTierType != ProductTierType.Enterprise) {
// Users without billing permission can't access billing // Users without billing permission can't access billing
if (!org.canEditSubscription) { if (!org.canEditSubscription) {
await this.dialogService.openSimpleDialog({ await dialogService.openSimpleDialog({
title: { key: "upgradeOrganizationEnterprise" }, title: { key: "upgradeOrganizationEnterprise" },
content: { key: "onlyAvailableForEnterpriseOrganization" }, content: { key: "onlyAvailableForEnterpriseOrganization" },
acceptButtonText: { key: "ok" }, acceptButtonText: { key: "ok" },
@ -47,7 +48,7 @@ export class IsEnterpriseOrgGuard implements CanActivate {
}); });
return false; return false;
} else { } else {
const upgradeConfirmed = await this.dialogService.openSimpleDialog({ const upgradeConfirmed = await dialogService.openSimpleDialog({
title: { key: "upgradeOrganizationEnterprise" }, title: { key: "upgradeOrganizationEnterprise" },
content: { key: "onlyAvailableForEnterpriseOrganization" }, content: { key: "onlyAvailableForEnterpriseOrganization" },
acceptButtonText: { key: "upgradeOrganization" }, acceptButtonText: { key: "upgradeOrganization" },
@ -55,7 +56,7 @@ export class IsEnterpriseOrgGuard implements CanActivate {
icon: "bwi-arrow-circle-up", icon: "bwi-arrow-circle-up",
}); });
if (upgradeConfirmed) { if (upgradeConfirmed) {
await this.router.navigate(["organizations", org.id, "billing", "subscription"], { await router.navigate(["organizations", org.id, "billing", "subscription"], {
queryParams: { upgrade: true, productTierType: ProductTierType.Enterprise }, queryParams: { upgrade: true, productTierType: ProductTierType.Enterprise },
}); });
} }
@ -63,5 +64,5 @@ export class IsEnterpriseOrgGuard implements CanActivate {
} }
return org.productTierType == ProductTierType.Enterprise; return org.productTierType == ProductTierType.Enterprise;
} };
} }

View File

@ -3,7 +3,7 @@ import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards"; import { AuthGuard } from "@bitwarden/angular/auth/guards";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { IsEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard"; import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard";
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard"; import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component"; import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
@ -72,7 +72,7 @@ const routes: Routes = [
data: { data: {
titleId: "memberAccessReport", titleId: "memberAccessReport",
}, },
canActivate: [IsEnterpriseOrgGuard], canActivate: [isEnterpriseOrgGuard()],
}, },
], ],
}, },