1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 10:33:57 +00:00
Files
browser/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts
Bernd Schoolmann af40ff26a2 [PM-21001] Move auth code to new encrypt service interface (#14542)
* Add new encrypt service functions

* Undo changes

* Cleanup

* Fix build

* Fix comments

* Move auth code to new encrypt service interface
2025-05-05 16:50:06 +02:00

243 lines
9.8 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { FakeGlobalState } from "@bitwarden/common/spec/fake-state";
import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { I18nService } from "../../core/i18n.service";
import {
AcceptOrganizationInviteService,
ORGANIZATION_INVITE,
} from "./accept-organization.service";
import { OrganizationInvite } from "./organization-invite";
describe("AcceptOrganizationInviteService", () => {
let sut: AcceptOrganizationInviteService;
let apiService: MockProxy<ApiService>;
let authService: MockProxy<AuthService>;
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
let policyService: MockProxy<PolicyService>;
let logService: MockProxy<LogService>;
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let i18nService: MockProxy<I18nService>;
let globalStateProvider: FakeGlobalStateProvider;
let globalState: FakeGlobalState<OrganizationInvite>;
let dialogService: MockProxy<DialogService>;
let accountService: MockProxy<AccountService>;
beforeEach(() => {
apiService = mock();
authService = mock();
keyService = mock();
encryptService = mock();
policyApiService = mock();
policyService = mock();
logService = mock();
organizationApiService = mock();
organizationUserApiService = mock();
i18nService = mock();
globalStateProvider = new FakeGlobalStateProvider();
globalState = globalStateProvider.getFake(ORGANIZATION_INVITE);
dialogService = mock();
accountService = mock();
sut = new AcceptOrganizationInviteService(
apiService,
authService,
keyService,
encryptService,
policyApiService,
policyService,
logService,
organizationApiService,
organizationUserApiService,
i18nService,
globalStateProvider,
dialogService,
accountService,
);
});
describe("validateAndAcceptInvite", () => {
it("initializes an organization when given an invite where initOrganization is true", async () => {
keyService.makeOrgKey.mockResolvedValue([
{ encryptedString: "string" } as EncString,
"orgPrivateKey" as unknown as OrgKey,
]);
keyService.makeKeyPair.mockResolvedValue([
"orgPublicKey",
{ encryptedString: "string" } as EncString,
]);
encryptService.wrapDecapsulationKey.mockResolvedValue({
encryptedString: "string",
} as EncString);
encryptService.encryptString.mockResolvedValue({ encryptedString: "string" } as EncString);
const invite = createOrgInvite({ initOrganization: true });
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled();
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(null);
expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
it("logs out the user and stores the invite when a master password policy check is required", async () => {
const invite = createOrgInvite();
policyApiService.getPoliciesByToken.mockResolvedValue([
{
type: PolicyType.MasterPassword,
enabled: true,
} as Policy,
]);
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(false);
expect(authService.logOut).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(invite);
});
it("clears the stored invite when a master password policy check is required but the stored invite doesn't match the provided one", async () => {
const storedInvite = createOrgInvite({ email: "wrongemail@example.com" });
const providedInvite = createOrgInvite();
await globalState.update(() => storedInvite);
policyApiService.getPoliciesByToken.mockResolvedValue([
{
type: PolicyType.MasterPassword,
enabled: true,
} as Policy,
]);
const result = await sut.validateAndAcceptInvite(providedInvite);
expect(result).toBe(false);
expect(authService.logOut).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(providedInvite);
});
it("accepts the invitation request when the organization doesn't have a master password policy", async () => {
const invite = createOrgInvite();
policyApiService.getPoliciesByToken.mockResolvedValue([]);
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(null);
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
it("accepts the invitation request when the org has a master password policy, but the user has already passed it and autoenroll is not enabled", async () => {
const invite = createOrgInvite();
policyApiService.getPoliciesByToken.mockResolvedValue([
{
type: PolicyType.MasterPassword,
enabled: true,
} as Policy,
]);
// an existing invite means the user has already passed the master password policy
await globalState.update(() => invite);
policyService.getResetPasswordPolicyOptions.mockReturnValue([
{
autoEnrollEnabled: false,
} as ResetPasswordPolicyOptions,
false,
]);
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
it("accepts the invitation request and enrolls when autoenroll is enabled", async () => {
const invite = createOrgInvite();
policyApiService.getPoliciesByToken.mockResolvedValue([
{
type: PolicyType.MasterPassword,
enabled: true,
} as Policy,
]);
organizationApiService.getKeys.mockResolvedValue(
new OrganizationKeysResponse({
privateKey: "privateKey",
publicKey: "publicKey",
}),
);
accountService.activeAccount$ = new BehaviorSubject({ id: "activeUserId" }) as any;
keyService.userKey$.mockReturnValue(new BehaviorSubject({ key: "userKey" } as any));
encryptService.encapsulateKeyUnsigned.mockResolvedValue({
encryptedString: "encryptedString",
} as EncString);
await globalState.update(() => invite);
policyService.getResetPasswordPolicyOptions.mockReturnValue([
{
autoEnrollEnabled: true,
} as ResetPasswordPolicyOptions,
true,
]);
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
{ key: "userKey" },
Utils.fromB64ToArray("publicKey"),
);
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
});
});
function createOrgInvite(custom: Partial<OrganizationInvite> = {}): OrganizationInvite {
return Object.assign(
{
email: "user@example.com",
initOrganization: false,
orgSsoIdentifier: null,
orgUserHasExistingUser: false,
organizationId: "organizationId",
organizationName: "organizationName",
organizationUserId: "organizationUserId",
token: "token",
},
custom,
);
}