mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Expand test coverage across all layers via 14 parallel agents: Unit tests (3,378 total, +534): - First-ever AI sidecar tests (157 tests covering bridge lifecycle, all 12 tool modules) - API route infrastructure (auth, pipeline, batch, settings, teams, roles, audit, api-keys, files, docs) - Lib coverage improvements (audit 7%->95%, worker-pool 33%->100%) - Web store/lib gap fills (features-store, tool-registry) Integration tests (4,403 total, +903): - Expanded 19 tool test files with parameter variations, format edge cases, boundary values - Cross-format matrix: 290 tests covering 14 tools x 17 formats - Adversarial/edge cases: 63 tests for extreme inputs, concurrent requests, corrupted files E2E-Docker (125 new tests): - Expanded 8 spec files + 1 new file covering all 49 tools - Added HEIC/format handling, auth failures, download verification GUI E2E (expanded 28 spec files): - Navigation, responsive layout, keyboard shortcuts - All 51 tool UIs with settings, processing, display modes - Batch/pipeline workflows, settings/RBAC, visual regression - Resilience, accessibility (ARIA, contrast, focus), performance budgets
326 lines
10 KiB
TypeScript
326 lines
10 KiB
TypeScript
/**
|
|
* Unit tests for roles route validation and role management logic.
|
|
*
|
|
* Tests the role name validation, builtin role protection,
|
|
* permission validation, and user reassignment on role deletion.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
// ── Reproduce validation logic from roles.ts ────────────────────────────
|
|
|
|
type Permission =
|
|
| "tools:use"
|
|
| "files:own"
|
|
| "files:all"
|
|
| "apikeys:own"
|
|
| "apikeys:all"
|
|
| "pipelines:own"
|
|
| "pipelines:all"
|
|
| "settings:read"
|
|
| "settings:write"
|
|
| "users:manage"
|
|
| "teams:manage"
|
|
| "features:manage"
|
|
| "system:health"
|
|
| "audit:read";
|
|
|
|
const ALL_PERMISSIONS: Permission[] = [
|
|
"tools:use",
|
|
"files:own",
|
|
"files:all",
|
|
"apikeys:own",
|
|
"apikeys:all",
|
|
"pipelines:own",
|
|
"pipelines:all",
|
|
"settings:read",
|
|
"settings:write",
|
|
"users:manage",
|
|
"teams:manage",
|
|
"features:manage",
|
|
"system:health",
|
|
"audit:read",
|
|
];
|
|
|
|
const ROLE_NAME_PATTERN = /^[a-z0-9_-]+$/;
|
|
|
|
function validateRoleName(raw: string): { success: boolean; name?: string; error?: string } {
|
|
const trimmed = raw.trim().toLowerCase();
|
|
if (trimmed.length < 2 || trimmed.length > 30) {
|
|
return { success: false, error: "Role name must be 2-30 characters" };
|
|
}
|
|
if (!ROLE_NAME_PATTERN.test(trimmed)) {
|
|
return {
|
|
success: false,
|
|
error: "Role name can only contain lowercase letters, numbers, hyphens, and underscores",
|
|
};
|
|
}
|
|
return { success: true, name: trimmed };
|
|
}
|
|
|
|
function validateCreateRole(body: {
|
|
name?: string;
|
|
description?: string;
|
|
permissions?: string[];
|
|
}): { success: boolean; error?: string } {
|
|
if (!body.name) return { success: false, error: "name is required" };
|
|
const nameResult = validateRoleName(body.name);
|
|
if (!nameResult.success) return nameResult;
|
|
if (!body.permissions || body.permissions.length === 0) {
|
|
return { success: false, error: "At least one permission is required" };
|
|
}
|
|
if (body.description !== undefined && body.description.length > 500) {
|
|
return { success: false, error: "Description too long" };
|
|
}
|
|
return { success: true };
|
|
}
|
|
|
|
function validateUpdateRole(body: {
|
|
name?: string;
|
|
description?: string;
|
|
permissions?: string[];
|
|
}): { success: boolean; error?: string } {
|
|
if (body.name !== undefined) {
|
|
const nameResult = validateRoleName(body.name);
|
|
if (!nameResult.success) return nameResult;
|
|
}
|
|
if (body.description !== undefined && body.description.length > 500) {
|
|
return { success: false, error: "Description too long" };
|
|
}
|
|
return { success: true };
|
|
}
|
|
|
|
function validatePermissions(permissions: string[]): string[] {
|
|
return permissions.filter((p) => !ALL_PERMISSIONS.includes(p as Permission));
|
|
}
|
|
|
|
// ── Tests ───────────────────────────────────────────────────────────────
|
|
|
|
describe("roles route logic", () => {
|
|
describe("role name validation", () => {
|
|
it("accepts valid lowercase name", () => {
|
|
const result = validateRoleName("viewer");
|
|
expect(result.success).toBe(true);
|
|
expect(result.name).toBe("viewer");
|
|
});
|
|
|
|
it("converts to lowercase", () => {
|
|
const result = validateRoleName("Viewer");
|
|
expect(result.success).toBe(true);
|
|
expect(result.name).toBe("viewer");
|
|
});
|
|
|
|
it("trims whitespace and lowercases", () => {
|
|
const result = validateRoleName(" Manager ");
|
|
expect(result.success).toBe(true);
|
|
expect(result.name).toBe("manager");
|
|
});
|
|
|
|
it("allows hyphens", () => {
|
|
const result = validateRoleName("team-lead");
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("allows underscores", () => {
|
|
const result = validateRoleName("team_lead");
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("allows numbers", () => {
|
|
const result = validateRoleName("admin2");
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("rejects single character", () => {
|
|
const result = validateRoleName("a");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects names longer than 30 characters", () => {
|
|
const result = validateRoleName("a".repeat(31));
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("accepts name exactly 30 characters", () => {
|
|
const result = validateRoleName("a".repeat(30));
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts name exactly 2 characters", () => {
|
|
const result = validateRoleName("ab");
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("rejects spaces in name", () => {
|
|
const result = validateRoleName("team lead");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects special characters", () => {
|
|
const result = validateRoleName("admin@role");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects dots in name", () => {
|
|
const result = validateRoleName("admin.role");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("create role validation", () => {
|
|
it("accepts valid create payload", () => {
|
|
const result = validateCreateRole({
|
|
name: "reviewer",
|
|
description: "Can review files",
|
|
permissions: ["tools:use", "files:own"],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("description is optional", () => {
|
|
const result = validateCreateRole({
|
|
name: "reviewer",
|
|
permissions: ["tools:use"],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("requires at least one permission", () => {
|
|
const result = validateCreateRole({
|
|
name: "reviewer",
|
|
permissions: [],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects missing permissions", () => {
|
|
const result = validateCreateRole({
|
|
name: "reviewer",
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects description longer than 500 characters", () => {
|
|
const result = validateCreateRole({
|
|
name: "reviewer",
|
|
permissions: ["tools:use"],
|
|
description: "x".repeat(501),
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("update role validation", () => {
|
|
it("accepts partial update with just name", () => {
|
|
const result = validateUpdateRole({ name: "new-name" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts partial update with just description", () => {
|
|
const result = validateUpdateRole({ description: "Updated description" });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts partial update with just permissions", () => {
|
|
const result = validateUpdateRole({ permissions: ["tools:use", "files:own"] });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts empty update body", () => {
|
|
const result = validateUpdateRole({});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("accepts all fields together", () => {
|
|
const result = validateUpdateRole({
|
|
name: "updated-role",
|
|
description: "New desc",
|
|
permissions: ["tools:use"],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("permission validation", () => {
|
|
it("returns empty array for valid permissions", () => {
|
|
const invalid = validatePermissions(["tools:use", "files:own"]);
|
|
expect(invalid).toHaveLength(0);
|
|
});
|
|
|
|
it("returns invalid permission strings", () => {
|
|
const invalid = validatePermissions(["tools:use", "invalid:perm", "also:bad"]);
|
|
expect(invalid).toEqual(["invalid:perm", "also:bad"]);
|
|
});
|
|
|
|
it("returns all permissions if all are invalid", () => {
|
|
const invalid = validatePermissions(["foo", "bar"]);
|
|
expect(invalid).toEqual(["foo", "bar"]);
|
|
});
|
|
|
|
it("handles empty array", () => {
|
|
const invalid = validatePermissions([]);
|
|
expect(invalid).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("ALL_PERMISSIONS constant", () => {
|
|
it("contains exactly 14 permissions", () => {
|
|
expect(ALL_PERMISSIONS).toHaveLength(14);
|
|
});
|
|
|
|
it("contains all expected permissions", () => {
|
|
expect(ALL_PERMISSIONS).toContain("tools:use");
|
|
expect(ALL_PERMISSIONS).toContain("files:own");
|
|
expect(ALL_PERMISSIONS).toContain("files:all");
|
|
expect(ALL_PERMISSIONS).toContain("apikeys:own");
|
|
expect(ALL_PERMISSIONS).toContain("apikeys:all");
|
|
expect(ALL_PERMISSIONS).toContain("settings:read");
|
|
expect(ALL_PERMISSIONS).toContain("settings:write");
|
|
expect(ALL_PERMISSIONS).toContain("users:manage");
|
|
expect(ALL_PERMISSIONS).toContain("teams:manage");
|
|
expect(ALL_PERMISSIONS).toContain("features:manage");
|
|
expect(ALL_PERMISSIONS).toContain("system:health");
|
|
expect(ALL_PERMISSIONS).toContain("audit:read");
|
|
});
|
|
});
|
|
|
|
describe("builtin role protection", () => {
|
|
it("blocks modification of builtin roles", () => {
|
|
const role = { isBuiltin: true, name: "admin" };
|
|
expect(role.isBuiltin).toBe(true);
|
|
});
|
|
|
|
it("allows modification of custom roles", () => {
|
|
const role = { isBuiltin: false, name: "reviewer" };
|
|
expect(role.isBuiltin).toBe(false);
|
|
});
|
|
|
|
it("blocks deletion of builtin roles", () => {
|
|
const role = { isBuiltin: true, name: "user" };
|
|
expect(role.isBuiltin).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("user reassignment on role deletion", () => {
|
|
it("reassigns users to 'user' role when custom role is deleted", () => {
|
|
const usersOnRole = [
|
|
{ id: "u1", role: "reviewer" },
|
|
{ id: "u2", role: "reviewer" },
|
|
];
|
|
const updatedUsers = usersOnRole.map((u) => ({ ...u, role: "user" }));
|
|
expect(updatedUsers.every((u) => u.role === "user")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("role name rename cascading", () => {
|
|
it("updates all users when role name changes", () => {
|
|
const users = [
|
|
{ id: "u1", role: "old-name" },
|
|
{ id: "u2", role: "old-name" },
|
|
];
|
|
const newName = "new-name";
|
|
const updated = users.map((u) => ({ ...u, role: newName }));
|
|
expect(updated.every((u) => u.role === "new-name")).toBe(true);
|
|
});
|
|
});
|
|
});
|