diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index f33a9cf347a..7d72dd30506 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -78,7 +78,6 @@ export class UnlockCommand { userId, this.keyConnectorService, this.environmentService, - this.syncService, this.organizationApiService, this.logout, ); diff --git a/apps/cli/src/commands/convert-to-key-connector.command.spec.ts b/apps/cli/src/commands/convert-to-key-connector.command.spec.ts new file mode 100644 index 00000000000..ed3d0e0810d --- /dev/null +++ b/apps/cli/src/commands/convert-to-key-connector.command.spec.ts @@ -0,0 +1,143 @@ +import { createPromptModule } from "inquirer"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { + Environment, + EnvironmentService, + Region, + Urls, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { Response } from "../models/response"; +import { MessageResponse } from "../models/response/message.response"; + +import { ConvertToKeyConnectorCommand } from "./convert-to-key-connector.command"; + +jest.mock("inquirer", () => { + return { + createPromptModule: jest.fn(() => jest.fn(() => Promise.resolve({ convert: "" }))), + }; +}); + +describe("ConvertToKeyConnectorCommand", () => { + let command: ConvertToKeyConnectorCommand; + + const userId = "test-user-id" as UserId; + const organization = { + id: "test-organization-id", + name: "Test Organization", + keyConnectorUrl: "https://keyconnector.example.com", + } as Organization; + + const keyConnectorService = mock(); + const environmentService = mock(); + const organizationApiService = mock(); + const logout = jest.fn(); + + beforeEach(async () => { + command = new ConvertToKeyConnectorCommand( + userId, + keyConnectorService, + environmentService, + organizationApiService, + logout, + ); + }); + + describe("run", () => { + it("should logout and return error response if no interaction available", async () => { + process.env.BW_NOINTERACTION = "true"; + + const response = await command.run(); + + expect(response).not.toBeNull(); + expect(response.success).toEqual(false); + expect(response).toEqual( + Response.error( + new MessageResponse( + "An organization you are a member of is using Key Connector. In order to access the vault, you must opt-in to Key Connector now via the web vault. You have been logged out.", + null, + ), + ), + ); + expect(logout).toHaveBeenCalled(); + }); + + it("should logout and return error response if interaction answer is exit", async () => { + process.env.BW_NOINTERACTION = "false"; + keyConnectorService.getManagingOrganization.mockResolvedValue(organization); + + (createPromptModule as jest.Mock).mockImplementation(() => + jest.fn(() => Promise.resolve({ convert: "exit" })), + ); + + const response = await command.run(); + + expect(response).not.toBeNull(); + expect(response.success).toEqual(false); + expect(response).toEqual(Response.error("You have been logged out.")); + expect(logout).toHaveBeenCalled(); + }); + + it("should key connector migrate user and return success response if answer is remove", async () => { + process.env.BW_NOINTERACTION = "false"; + keyConnectorService.getManagingOrganization.mockResolvedValue(organization); + environmentService.environment$ = of({ + getUrls: () => + ({ + keyConnector: "old-key-connector-url", + }) as Urls, + } as Environment); + + (createPromptModule as jest.Mock).mockImplementation(() => + jest.fn(() => Promise.resolve({ convert: "remove" })), + ); + + const response = await command.run(); + + expect(response).not.toBeNull(); + expect(response.success).toEqual(true); + expect(keyConnectorService.migrateUser).toHaveBeenCalledWith(userId); + expect(keyConnectorService.removeConvertAccountRequired).toHaveBeenCalledWith(userId); + expect(keyConnectorService.setUsesKeyConnector).toHaveBeenCalledWith(true, userId); + expect(environmentService.setEnvironment).toHaveBeenCalledWith(Region.SelfHosted, { + keyConnector: organization.keyConnectorUrl, + } as Urls); + }); + + it("should logout and throw error if key connector migrate user fails", async () => { + process.env.BW_NOINTERACTION = "false"; + keyConnectorService.getManagingOrganization.mockResolvedValue(organization); + + (createPromptModule as jest.Mock).mockImplementation(() => + jest.fn(() => Promise.resolve({ convert: "remove" })), + ); + + keyConnectorService.migrateUser.mockRejectedValue(new Error("Migration failed")); + + await expect(command.run()).rejects.toThrow("Migration failed"); + expect(logout).toHaveBeenCalled(); + }); + + it("should leave organization and return success response if answer is leave", async () => { + process.env.BW_NOINTERACTION = "false"; + keyConnectorService.getManagingOrganization.mockResolvedValue(organization); + + (createPromptModule as jest.Mock).mockImplementation(() => + jest.fn(() => Promise.resolve({ convert: "leave" })), + ); + + const response = await command.run(); + + expect(response).not.toBeNull(); + expect(response.success).toEqual(true); + expect(organizationApiService.leave).toHaveBeenCalledWith(organization.id); + expect(keyConnectorService.removeConvertAccountRequired).toHaveBeenCalledWith(userId); + }); + }); +}); diff --git a/apps/cli/src/commands/convert-to-key-connector.command.ts b/apps/cli/src/commands/convert-to-key-connector.command.ts index 65c793bf337..a87483af625 100644 --- a/apps/cli/src/commands/convert-to-key-connector.command.ts +++ b/apps/cli/src/commands/convert-to-key-connector.command.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import * as inquirer from "inquirer"; import { firstValueFrom } from "rxjs"; @@ -10,7 +8,6 @@ import { Region, } from "@bitwarden/common/platform/abstractions/environment.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Response } from "../models/response"; import { MessageResponse } from "../models/response/message.response"; @@ -20,7 +17,6 @@ export class ConvertToKeyConnectorCommand { private readonly userId: UserId, private keyConnectorService: KeyConnectorService, private environmentService: EnvironmentService, - private syncService: SyncService, private organizationApiService: OrganizationApiServiceAbstraction, private logout: () => Promise, ) {} diff --git a/apps/cli/src/models/response/message.response.ts b/apps/cli/src/models/response/message.response.ts index dcbe3b92216..889d0982f83 100644 --- a/apps/cli/src/models/response/message.response.ts +++ b/apps/cli/src/models/response/message.response.ts @@ -5,11 +5,11 @@ import { BaseResponse } from "./base.response"; export class MessageResponse implements BaseResponse { object: string; title: string; - message: string; + message: string | null; raw: string; noColor = false; - constructor(title: string, message: string) { + constructor(title: string, message: string | null) { this.object = "message"; this.title = title; this.message = message;