diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index ab9976b985c..22ca27bb311 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -50,10 +50,8 @@ import { } from "../../../shared/components/access-selector"; import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator"; -import { - orgSeatLimitReachedValidator, - inputEmailLimitValidator, -} from "./validators/org-seat-limit-reached.validator"; +import { inputEmailLimitValidator } from "./validators/input-email-limit.validator"; +import { orgSeatLimitReachedValidator } from "./validators/org-seat-limit-reached.validator"; export enum MemberDialogTab { Role = 0, @@ -273,7 +271,7 @@ export class MemberDialogComponent implements OnDestroy { } private setFormValidators(organization: Organization) { - const emailsControlValidators = [ + const _orgSeatLimitReachedValidator = [ Validators.required, commaSeparatedEmails, orgSeatLimitReachedValidator( @@ -283,8 +281,17 @@ export class MemberDialogComponent implements OnDestroy { ), ]; + const _inputEmailLimitValidator = [ + Validators.required, + commaSeparatedEmails, + inputEmailLimitValidator(organization, (maxEmailsCount: number) => + this.i18nService.t("tooManyEmails", maxEmailsCount), + ), + ]; + const emailsControl = this.formGroup.get("emails"); - emailsControl.setValidators(emailsControlValidators); + emailsControl.setValidators(_orgSeatLimitReachedValidator); + emailsControl.setValidators(_inputEmailLimitValidator); emailsControl.updateValueAndValidity(); } @@ -478,8 +485,6 @@ export class MemberDialogComponent implements OnDestroy { private async handleInviteUsers(userView: OrganizationUserAdminView, organization: Organization) { const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))]; - this.setInputEmailCountValidator(organization, emails.length); - await this.userService.invite(emails, userView); this.toastService.showToast({ @@ -490,20 +495,6 @@ export class MemberDialogComponent implements OnDestroy { this.close(MemberDialogResult.Saved); } - private setInputEmailCountValidator(organization: Organization, emailCount: number) { - const emailsControlValidators = [ - Validators.required, - commaSeparatedEmails, - inputEmailLimitValidator(organization, (maxEmailsCount: number) => - this.i18nService.t("tooManyEmails", maxEmailsCount), - ), - ]; - - const emailsControl = this.formGroup.get("emails"); - emailsControl.setValidators(emailsControlValidators); - emailsControl.updateValueAndValidity(); - } - remove = async () => { if (!this.editMode) { return; diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/input-email-limit.validator.spec.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/input-email-limit.validator.spec.ts new file mode 100644 index 00000000000..5a9a0e128e7 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/input-email-limit.validator.spec.ts @@ -0,0 +1,191 @@ +import { AbstractControl, FormControl } from "@angular/forms"; + +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 { inputEmailLimitValidator } from "./input-email-limit.validator"; + +const orgFactory = (props: Partial = {}) => + Object.assign( + new Organization(), + { + id: "myOrgId", + enabled: true, + type: OrganizationUserType.Admin, + }, + props, + ); + +describe("inputEmailLimitValidator", () => { + const getErrorMessage = (max: number) => `You can only add up to ${max} unique emails.`; + + const createUniqueEmailString = (numberOfEmails: number) => + Array(numberOfEmails) + .fill(null) + .map((_, i) => `email${i}@example.com`) + .join(", "); + + const createIdenticalEmailString = (numberOfEmails: number) => + Array(numberOfEmails) + .fill(null) + .map(() => `email@example.com`) + .join(", "); + + describe("TeamsStarter limit validation", () => { + let teamsStarterOrganization: Organization; + + beforeEach(() => { + teamsStarterOrganization = orgFactory({ + productTierType: ProductTierType.TeamsStarter, + seats: 10, + }); + }); + + it("should return null if unique email count is within the limit", () => { + // Arrange + const control = new FormControl(createUniqueEmailString(3)); + + const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toBeNull(); + }); + + it("should return null if unique email count is equal the limit", () => { + // Arrange + const control = new FormControl(createUniqueEmailString(10)); + + const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toBeNull(); + }); + + it("should return an error if unique email count exceeds the limit", () => { + // Arrange + const control = new FormControl(createUniqueEmailString(11)); + + const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toEqual({ + tooManyEmails: { message: "You can only add up to 10 unique emails." }, + }); + }); + }); + + describe("Non-TeamsStarter limit validation", () => { + let nonTeamsStarterOrganization: Organization; + + beforeEach(() => { + nonTeamsStarterOrganization = orgFactory({ + productTierType: ProductTierType.Enterprise, + seats: 100, + }); + }); + + it("should return null if unique email count is within the limit", () => { + // Arrange + const control = new FormControl(createUniqueEmailString(3)); + + const validatorFn = inputEmailLimitValidator(nonTeamsStarterOrganization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toBeNull(); + }); + + it("should return null if unique email count is equal the limit", () => { + // Arrange + const control = new FormControl(createUniqueEmailString(10)); + + const validatorFn = inputEmailLimitValidator(nonTeamsStarterOrganization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toBeNull(); + }); + + it("should return an error if unique email count exceeds the limit", () => { + // Arrange + + const control = new FormControl(createUniqueEmailString(21)); + + const validatorFn = inputEmailLimitValidator(nonTeamsStarterOrganization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toEqual({ + tooManyEmails: { message: "You can only add up to 20 unique emails." }, + }); + }); + }); + + describe("input email validation", () => { + let organization: Organization; + + beforeEach(() => { + organization = orgFactory({ + productTierType: ProductTierType.Enterprise, + seats: 100, + }); + }); + + it("should ignore duplicate emails and validate only unique ones", () => { + // Arrange + const sixUniqueEmails = createUniqueEmailString(6); + const sixDuplicateEmails = createIdenticalEmailString(6); + + const control = new FormControl(sixUniqueEmails + sixDuplicateEmails); + const validatorFn = inputEmailLimitValidator(organization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toBeNull(); + }); + + it("should return null if input is null", () => { + // Arrange + const control: AbstractControl = new FormControl(null); + + const validatorFn = inputEmailLimitValidator(organization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toBeNull(); + }); + + it("should return null if input is empty", () => { + // Arrange + const control: AbstractControl = new FormControl(""); + + const validatorFn = inputEmailLimitValidator(organization, getErrorMessage); + + // Act + const result = validatorFn(control); + + // Assert + expect(result).toBeNull(); + }); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/input-email-limit.validator.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/input-email-limit.validator.ts new file mode 100644 index 00000000000..f34c2e909aa --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/input-email-limit.validator.ts @@ -0,0 +1,40 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; + +function getUniqueInputEmails(control: AbstractControl): string[] { + const emails: string[] = control.value + .split(",") + .filter((email: string) => email && email.trim() !== ""); + const uniqueEmails: string[] = Array.from(new Set(emails)); + + return uniqueEmails; +} + +/** + * Ensure the number of unique emails in an input does not exceed the allowed maximum. + * @param organization An object representing the organization + * @param getErrorMessage A callback function that generates the error message. It takes the `maxEmailsCount` as a parameter. + * @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null` + */ +export function inputEmailLimitValidator( + organization: Organization, + getErrorMessage: (maxEmailsCount: number) => string, +): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value?.trim()) { + return null; + } + + const maxEmailsCount = organization.productTierType === ProductTierType.TeamsStarter ? 10 : 20; + + const uniqueEmails = getUniqueInputEmails(control); + + if (uniqueEmails.length <= maxEmailsCount) { + return null; + } + + return { tooManyEmails: { message: getErrorMessage(maxEmailsCount) } }; + }; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts index 7b41c839dc0..6c693ee8f84 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.spec.ts @@ -4,10 +4,7 @@ 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 { - inputEmailLimitValidator, - orgSeatLimitReachedValidator, -} from "./org-seat-limit-reached.validator"; +import { orgSeatLimitReachedValidator } from "./org-seat-limit-reached.validator"; const orgFactory = (props: Partial = {}) => Object.assign( @@ -134,171 +131,3 @@ describe("orgSeatLimitReachedValidator", () => { expect(result).toBeNull(); }); }); - -describe("inputEmailLimitValidator", () => { - const getErrorMessage = (max: number) => `You can only add up to ${max} unique emails.`; - - const createUniqueEmailString = (numberOfEmails: number) => { - let result = ""; - - for (let i = 1; i <= numberOfEmails; i++) { - result += `test${i}@test.com, `; - } - - // Remove the last comma and space - result = result.slice(0, -1); - - return result; - }; - - describe("TeamsStarter limit validation", () => { - const teamsStarterOrganization = orgFactory({ - productTierType: ProductTierType.TeamsStarter, - seats: 10, - }); - - it("should return null if unique email count is within the limit", () => { - // Arrange - const control = new FormControl("test1@test.com, test2@test.com, test3@test.com"); - - const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toBeNull(); - }); - - it("should return null if unique email count is equal the limit", () => { - // Arrange - const control = new FormControl( - "test1@test.com, test2@test.com, test3@test.com, test4@test.com, test5@test.com, test6@test.com, test7@test.com, test8@test.com, test9@test.com, test10@test.com", - ); - - const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toBeNull(); - }); - - it("should return an error if unique email count exceeds the limit", () => { - // Arrange - const control = new FormControl(createUniqueEmailString(11)); - - const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toEqual({ - tooManyEmails: { message: "You can only add up to 10 unique emails." }, - }); - }); - }); - - describe("none TeamsStarter limit validation", () => { - const noneTeamsStarterOrganization = orgFactory({ - productTierType: ProductTierType.Enterprise, - seats: 100, - }); - - it("should return null if unique email count is within the limit", () => { - // Arrange - const control = new FormControl("test1@test.com, test2@test.com, test3@test.com"); - - const validatorFn = inputEmailLimitValidator(noneTeamsStarterOrganization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toBeNull(); - }); - - it("should return null if unique email count is equal the limit", () => { - // Arrange - const control = new FormControl( - "test1@test.com, test2@test.com, test3@test.com, test4@test.com, test5@test.com, test6@test.com, test7@test.com, test8@test.com, test9@test.com, test10@test.com", - ); - - const validatorFn = inputEmailLimitValidator(noneTeamsStarterOrganization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toBeNull(); - }); - - it("should return an error if unique email count exceeds the limit", () => { - // Arrange - - const control = new FormControl(createUniqueEmailString(21)); - - const validatorFn = inputEmailLimitValidator(noneTeamsStarterOrganization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toEqual({ - tooManyEmails: { message: "You can only add up to 20 unique emails." }, - }); - }); - }); - - describe("input email validation", () => { - const organization = orgFactory({ - productTierType: ProductTierType.Enterprise, - seats: 100, - }); - - it("should ignore duplicate emails and validate only unique ones", () => { - // Arrange - - const sixUniqueEmails = createUniqueEmailString(6); - const sixDuplicateEmails = - "test1@test.com,test1@test.com,test1@test.com,test1@test.com,test1@test.com,test1@test.com"; - - const control = new FormControl(sixUniqueEmails + sixDuplicateEmails); - const validatorFn = inputEmailLimitValidator(organization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toBeNull(); - }); - - it("should return null if input is null", () => { - // Arrange - const control: AbstractControl = new FormControl(null); - - const validatorFn = inputEmailLimitValidator(organization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toBeNull(); - }); - - it("should return null if input is empty", () => { - // Arrange - const control: AbstractControl = new FormControl(""); - - const validatorFn = inputEmailLimitValidator(organization, getErrorMessage); - - // Act - const result = validatorFn(control); - - // Assert - expect(result).toBeNull(); - }); - }); -}); diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts index 838499653c7..bcd84743918 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator.ts @@ -47,39 +47,3 @@ export function orgSeatLimitReachedValidator( : null; }; } - -function getUniqueInputEmails(control: AbstractControl): string[] { - const emails: string[] = control.value - .split(",") - .filter((email: string) => email && email.trim() !== ""); - const uniqueEmails: string[] = Array.from(new Set(emails)); - - return uniqueEmails; -} - -/** - * Ensure the number of unique emails in an input does not exceed the allowed maximum. - * @param organization An object representing the organization - * @param getErrorMessage A callback function that generates the error message. It takes the `maxEmailsCount` as a parameter. - * @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null` - */ -export function inputEmailLimitValidator( - organization: Organization, - getErrorMessage: (maxEmailsCount: number) => string, -): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - if (control.value === "" || !control.value) { - return null; - } - - const maxEmailsCount = organization.productTierType === ProductTierType.TeamsStarter ? 10 : 20; - - const uniqueEmails = getUniqueInputEmails(control); - - if (uniqueEmails.length <= maxEmailsCount) { - return null; - } - - return { tooManyEmails: { message: getErrorMessage(maxEmailsCount) } }; - }; -}