Files
stack/apps/web/src/components/workspace/validation.test.ts
Jason Woltje 6d92251fc1
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix(SEC-WEB-27+28): Robust email validation + role cast validation
SEC-WEB-27: Replace weak email.includes('@') check with RFC 5322-aligned
programmatic validation (isValidEmail). Uses character-level domain label
validation to avoid ReDoS vulnerabilities from complex regex patterns.

SEC-WEB-28: Replace unsafe 'as WorkspaceMemberRole' type casts with
runtime validation (toWorkspaceMemberRole) that checks against known enum
values and falls back to MEMBER for invalid inputs. Applied in both
InviteMember.tsx and MemberList.tsx.

Adds 43 tests covering validation logic, InviteMember component, and
MemberList component behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 15:40:05 -06:00

135 lines
4.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { WorkspaceMemberRole } from "@mosaic/shared";
import { isValidEmail, toWorkspaceMemberRole } from "./validation";
describe("isValidEmail", (): void => {
describe("valid emails", (): void => {
it("should accept a standard email", (): void => {
expect(isValidEmail("user@example.com")).toBe(true);
});
it("should accept email with dots in local part", (): void => {
expect(isValidEmail("first.last@example.com")).toBe(true);
});
it("should accept email with plus addressing", (): void => {
expect(isValidEmail("user+tag@example.com")).toBe(true);
});
it("should accept email with subdomain", (): void => {
expect(isValidEmail("user@mail.example.com")).toBe(true);
});
it("should accept email with hyphen in domain", (): void => {
expect(isValidEmail("user@my-domain.com")).toBe(true);
});
it("should accept email with special characters in local part", (): void => {
expect(isValidEmail("user!#$%&'*+/=?^_`{|}~@example.com")).toBe(true);
});
it("should accept email with numbers", (): void => {
expect(isValidEmail("user123@example456.com")).toBe(true);
});
it("should accept single-character local part", (): void => {
expect(isValidEmail("a@example.com")).toBe(true);
});
});
describe("invalid emails", (): void => {
it("should reject empty string", (): void => {
expect(isValidEmail("")).toBe(false);
});
it("should reject email without @", (): void => {
expect(isValidEmail("userexample.com")).toBe(false);
});
it("should reject email with only @", (): void => {
expect(isValidEmail("@")).toBe(false);
});
it("should reject email without local part", (): void => {
expect(isValidEmail("@example.com")).toBe(false);
});
it("should reject email without domain", (): void => {
expect(isValidEmail("user@")).toBe(false);
});
it("should reject email with spaces", (): void => {
expect(isValidEmail("user @example.com")).toBe(false);
});
it("should reject email with multiple @ signs", (): void => {
expect(isValidEmail("user@@example.com")).toBe(false);
});
it("should reject email with domain starting with hyphen", (): void => {
expect(isValidEmail("user@-example.com")).toBe(false);
});
it("should reject email exceeding 254 characters", (): void => {
const longLocal = "a".repeat(243);
const longEmail = `${longLocal}@example.com`;
expect(longEmail.length).toBeGreaterThan(254);
expect(isValidEmail(longEmail)).toBe(false);
});
it("should reject email that only contains @", (): void => {
expect(isValidEmail("just@")).toBe(false);
});
it("should reject plaintext without any structure", (): void => {
expect(isValidEmail("not-an-email")).toBe(false);
});
});
});
describe("toWorkspaceMemberRole", (): void => {
describe("valid roles", (): void => {
it("should return OWNER for 'OWNER'", (): void => {
expect(toWorkspaceMemberRole("OWNER")).toBe(WorkspaceMemberRole.OWNER);
});
it("should return ADMIN for 'ADMIN'", (): void => {
expect(toWorkspaceMemberRole("ADMIN")).toBe(WorkspaceMemberRole.ADMIN);
});
it("should return MEMBER for 'MEMBER'", (): void => {
expect(toWorkspaceMemberRole("MEMBER")).toBe(WorkspaceMemberRole.MEMBER);
});
it("should return GUEST for 'GUEST'", (): void => {
expect(toWorkspaceMemberRole("GUEST")).toBe(WorkspaceMemberRole.GUEST);
});
});
describe("invalid roles", (): void => {
it("should fall back to MEMBER for empty string", (): void => {
expect(toWorkspaceMemberRole("")).toBe(WorkspaceMemberRole.MEMBER);
});
it("should fall back to MEMBER for unknown role", (): void => {
expect(toWorkspaceMemberRole("SUPERADMIN")).toBe(WorkspaceMemberRole.MEMBER);
});
it("should fall back to MEMBER for lowercase variant", (): void => {
expect(toWorkspaceMemberRole("admin")).toBe(WorkspaceMemberRole.MEMBER);
});
it("should fall back to MEMBER for mixed case", (): void => {
expect(toWorkspaceMemberRole("Admin")).toBe(WorkspaceMemberRole.MEMBER);
});
it("should fall back to MEMBER for numeric input", (): void => {
expect(toWorkspaceMemberRole("123")).toBe(WorkspaceMemberRole.MEMBER);
});
it("should fall back to MEMBER for special characters", (): void => {
expect(toWorkspaceMemberRole("<script>")).toBe(WorkspaceMemberRole.MEMBER);
});
});
});