fix(SEC-WEB-27+28): Robust email validation + role cast validation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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>
This commit is contained in:
109
apps/web/src/components/workspace/MemberList.test.tsx
Normal file
109
apps/web/src/components/workspace/MemberList.test.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { WorkspaceMemberRole } from "@mosaic/shared";
|
||||
import { MemberList } from "./MemberList";
|
||||
import type { WorkspaceMemberWithUser } from "./MemberList";
|
||||
|
||||
const makeMember = (
|
||||
overrides: Partial<WorkspaceMemberWithUser> & { userId: string }
|
||||
): WorkspaceMemberWithUser => ({
|
||||
workspaceId: overrides.workspaceId ?? "ws-1",
|
||||
userId: overrides.userId,
|
||||
role: overrides.role ?? WorkspaceMemberRole.MEMBER,
|
||||
joinedAt: overrides.joinedAt ?? new Date("2025-01-01"),
|
||||
user: overrides.user ?? {
|
||||
id: overrides.userId,
|
||||
name: `User ${overrides.userId}`,
|
||||
email: `${overrides.userId}@example.com`,
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
authProviderId: `auth-${overrides.userId}`,
|
||||
preferences: {},
|
||||
createdAt: new Date("2025-01-01"),
|
||||
updatedAt: new Date("2025-01-01"),
|
||||
},
|
||||
});
|
||||
|
||||
describe("MemberList", (): void => {
|
||||
const mockOnRoleChange = vi.fn<(userId: string, newRole: WorkspaceMemberRole) => Promise<void>>();
|
||||
const mockOnRemove = vi.fn<(userId: string) => Promise<void>>();
|
||||
|
||||
const defaultProps = {
|
||||
currentUserId: "user-1",
|
||||
currentUserRole: WorkspaceMemberRole.ADMIN,
|
||||
workspaceOwnerId: "owner-1",
|
||||
onRoleChange: mockOnRoleChange,
|
||||
onRemove: mockOnRemove,
|
||||
};
|
||||
|
||||
beforeEach((): void => {
|
||||
mockOnRoleChange.mockReset();
|
||||
mockOnRoleChange.mockResolvedValue(undefined);
|
||||
mockOnRemove.mockReset();
|
||||
mockOnRemove.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("should render member list with correct count", (): void => {
|
||||
const members = [makeMember({ userId: "user-1" }), makeMember({ userId: "user-2" })];
|
||||
render(<MemberList {...defaultProps} members={members} />);
|
||||
expect(screen.getByText("Members (2)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display member name and email", (): void => {
|
||||
const members = [
|
||||
makeMember({
|
||||
userId: "user-2",
|
||||
user: {
|
||||
id: "user-2",
|
||||
name: "Jane Doe",
|
||||
email: "jane@example.com",
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
authProviderId: "auth-2",
|
||||
preferences: {},
|
||||
createdAt: new Date("2025-01-01"),
|
||||
updatedAt: new Date("2025-01-01"),
|
||||
},
|
||||
}),
|
||||
];
|
||||
render(<MemberList {...defaultProps} members={members} />);
|
||||
expect(screen.getByText("Jane Doe")).toBeInTheDocument();
|
||||
expect(screen.getByText("jane@example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show (you) indicator for current user", (): void => {
|
||||
const members = [makeMember({ userId: "user-1" })];
|
||||
render(<MemberList {...defaultProps} members={members} />);
|
||||
expect(screen.getByText("(you)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onRoleChange with validated role when admin changes a member role", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
const members = [
|
||||
makeMember({ userId: "user-1" }),
|
||||
makeMember({ userId: "user-2", role: WorkspaceMemberRole.MEMBER }),
|
||||
];
|
||||
render(<MemberList {...defaultProps} members={members} />);
|
||||
|
||||
const roleSelect = screen.getByDisplayValue("Member");
|
||||
await user.selectOptions(roleSelect, WorkspaceMemberRole.GUEST);
|
||||
|
||||
expect(mockOnRoleChange).toHaveBeenCalledWith("user-2", WorkspaceMemberRole.GUEST);
|
||||
});
|
||||
|
||||
it("should not show role select for the workspace owner", (): void => {
|
||||
const members = [
|
||||
makeMember({ userId: "owner-1", role: WorkspaceMemberRole.OWNER }),
|
||||
makeMember({ userId: "user-1", role: WorkspaceMemberRole.ADMIN }),
|
||||
];
|
||||
render(<MemberList {...defaultProps} members={members} />);
|
||||
expect(screen.getByText("OWNER")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show remove button for the workspace owner", (): void => {
|
||||
const members = [makeMember({ userId: "owner-1", role: WorkspaceMemberRole.OWNER })];
|
||||
render(<MemberList {...defaultProps} members={members} />);
|
||||
expect(screen.queryByLabelText("Remove member")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user