Skip to content
386 changes: 386 additions & 0 deletions apps/cli/src/tools/send/commands/create.command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock } from "jest-mock-extended";
import { of } from "rxjs";

import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { AuthType } from "@bitwarden/common/tools/send/models/domain/send";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/user-core";

import { SendCreateCommand } from "./create.command";

describe("SendCreateCommand", () => {
let command: SendCreateCommand;

const sendService = mock<SendService>();
const environmentService = mock<EnvironmentService>();
const sendApiService = mock<SendApiService>();
const accountProfileService = mock<BillingAccountProfileStateService>();
const accountService = mock<AccountService>();

const activeAccount = {
id: "user-id" as UserId,
...mockAccountInfoWith({
email: "[email protected]",
name: "User",
}),
};

beforeEach(() => {
jest.clearAllMocks();

accountService.activeAccount$ = of(activeAccount);
accountProfileService.hasPremiumFromAnySource$.mockReturnValue(of(false));
environmentService.environment$ = of({
getWebVaultUrl: () => "https://vault.bitwarden.com",
} as any);

command = new SendCreateCommand(
sendService,
environmentService,
sendApiService,
accountProfileService,
accountService,
);
});

describe("authType inference", () => {
const futureDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

describe("with CLI flags", () => {
it("should set authType to Email when emails are provided via CLI", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
};

const cmdOptions = {
email: ["[email protected]"],
};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", emails: "[email protected]", authType: AuthType.Email } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(true);
expect(sendService.encrypt).toHaveBeenCalledWith(
expect.objectContaining({
type: SendType.Text,
}),
null,
undefined,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ This doesn't yet verify that authType was set correctly. There should be another check here of what was passed to the API the same way the Edit command tests do (lines 96-98 of edit.command.spec.ts). Same for all the tests in this file

const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.Email);
expect(savedCall[0].emails).toBe("[email protected]");
});

it("should set authType to Password when password is provided via CLI", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
};

const cmdOptions = {
password: "testPassword123",
};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", authType: AuthType.Password } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(true);
expect(sendService.encrypt).toHaveBeenCalledWith(
expect.any(Object),
null as any,
"testPassword123",
);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.Password);
});

it("should set authType to None when neither emails nor password provided", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
};

const cmdOptions = {};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", authType: AuthType.None } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(true);
expect(sendService.encrypt).toHaveBeenCalledWith(expect.any(Object), null, undefined);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.None);
});

it("should return error when both emails and password provided via CLI", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
};

const cmdOptions = {
email: ["[email protected]"],
password: "testPassword123",
};

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(false);
expect(response.message).toBe("--password and --emails are mutually exclusive.");
});
});

describe("with JSON input", () => {
it("should set authType to Email when emails provided in JSON", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
emails: ["[email protected]", "[email protected]"],
};

sendService.encrypt.mockResolvedValue([
{
id: "send-id",
emails: "[email protected],[email protected]",
authType: AuthType.Email,
} as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, {});

expect(response.success).toBe(true);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.Email);
expect(savedCall[0].emails).toBe("[email protected],[email protected]");
});

it("should set authType to Password when password provided in JSON", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
password: "jsonPassword123",
};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", authType: AuthType.Password } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, {});

expect(response.success).toBe(true);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.Password);
});

it("should return error when both emails and password provided in JSON", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
emails: ["[email protected]"],
password: "jsonPassword123",
};

const response = await command.run(requestJson, {});

expect(response.success).toBe(false);
expect(response.message).toBe("--password and --emails are mutually exclusive.");
});
});

describe("with mixed CLI and JSON input", () => {
it("should return error when CLI emails combined with JSON password", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
password: "jsonPassword123",
};

const cmdOptions = {
email: ["[email protected]"],
};

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(false);
expect(response.message).toBe("--password and --emails are mutually exclusive.");
});

it("should return error when CLI password combined with JSON emails", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
emails: ["[email protected]"],
};

const cmdOptions = {
password: "cliPassword123",
};

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(false);
expect(response.message).toBe("--password and --emails are mutually exclusive.");
});

it("should use CLI value when JSON has different value of same type", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
emails: ["[email protected]"],
};

const cmdOptions = {
email: ["[email protected]"],
};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", emails: "[email protected]", authType: AuthType.Email } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(true);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.Email);
expect(savedCall[0].emails).toBe("[email protected]");
});
});

describe("edge cases", () => {
it("should set authType to None when emails array is empty", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
emails: [] as string[],
};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", authType: AuthType.None } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, {});

expect(response.success).toBe(true);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.None);
});

it("should set authType to None when password is empty string", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
};

const cmdOptions = {
password: "",
};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", authType: AuthType.None } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(true);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.None);
});

it("should set authType to None when password is whitespace only", async () => {
const requestJson = {
type: SendType.Text,
text: { text: "test content", hidden: false },
deletionDate: futureDate,
};

const cmdOptions = {
password: " ",
};

sendService.encrypt.mockResolvedValue([
{ id: "send-id", authType: AuthType.None } as any,
null as any,
]);
sendApiService.save.mockResolvedValue(undefined as any);
sendService.getFromState.mockResolvedValue({
decrypt: jest.fn().mockResolvedValue({}),
} as any);

const response = await command.run(requestJson, cmdOptions);

expect(response.success).toBe(true);
const savedCall = sendApiService.save.mock.calls[0][0];
expect(savedCall[0].authType).toBe(AuthType.None);
});
});
});
});
Loading
Loading