mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
Merge pull request #469 from drifterza/feature/oidc-pending-approval
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("react-router", () => ({
|
||||
useRevalidator: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("usehooks-ts", () => ({
|
||||
useInterval: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("react", async () => {
|
||||
const actual = await vi.importActual("react");
|
||||
return {
|
||||
...actual,
|
||||
createContext: vi.fn(() => ({ Provider: vi.fn() })),
|
||||
useContext: vi.fn(),
|
||||
useEffect: vi.fn(),
|
||||
useState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("LiveDataProvider", () => {
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("refresh interval", () => {
|
||||
test("uses 3 second interval", async () => {
|
||||
const { useInterval } = await import("usehooks-ts");
|
||||
expect(useInterval).toBeDefined();
|
||||
expect(3000).toBe(3000);
|
||||
});
|
||||
|
||||
test("disables interval when paused", () => {
|
||||
const visible = true;
|
||||
const paused = true;
|
||||
const interval = visible && !paused ? 3000 : null;
|
||||
expect(interval).toBeNull();
|
||||
});
|
||||
|
||||
test("disables interval when hidden", () => {
|
||||
const visible = false;
|
||||
const paused = false;
|
||||
const interval = visible && !paused ? 3000 : null;
|
||||
expect(interval).toBeNull();
|
||||
});
|
||||
|
||||
test("only revalidates when idle", () => {
|
||||
const mockRevalidate = vi.fn();
|
||||
const revalidateIfIdle = (state: string) => {
|
||||
if (state === "idle") mockRevalidate();
|
||||
};
|
||||
|
||||
revalidateIfIdle("idle");
|
||||
expect(mockRevalidate).toHaveBeenCalledTimes(1);
|
||||
|
||||
mockRevalidate.mockClear();
|
||||
revalidateIfIdle("loading");
|
||||
expect(mockRevalidate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useLiveData hook", () => {
|
||||
test("returns pause and resume functions", () => {
|
||||
const mockSetPaused = vi.fn();
|
||||
const hook = {
|
||||
pause: () => mockSetPaused(true),
|
||||
resume: () => mockSetPaused(false),
|
||||
};
|
||||
|
||||
hook.pause();
|
||||
expect(mockSetPaused).toHaveBeenCalledWith(true);
|
||||
|
||||
hook.resume();
|
||||
expect(mockSetPaused).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pending approval page", () => {
|
||||
test("redirects when user has access", () => {
|
||||
const hasAccess = true;
|
||||
const redirect = hasAccess ? "/machines" : null;
|
||||
expect(redirect).toBe("/machines");
|
||||
});
|
||||
|
||||
test("stays on page when user lacks access", () => {
|
||||
const hasAccess = false;
|
||||
const redirect = hasAccess ? "/machines" : null;
|
||||
expect(redirect).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { Capabilities, hasCapability, Roles, getRoleFromCapabilities } from "~/server/web/roles";
|
||||
|
||||
describe("Roles and Capabilities", () => {
|
||||
describe("Roles definitions", () => {
|
||||
test("owner has all capabilities including ui_access", () => {
|
||||
expect(Roles.owner & Capabilities.ui_access).toBe(Capabilities.ui_access);
|
||||
expect(Roles.owner & Capabilities.owner).toBe(Capabilities.owner);
|
||||
expect(Roles.owner & Capabilities.write_users).toBe(Capabilities.write_users);
|
||||
});
|
||||
|
||||
test("admin has ui_access but not owner flag", () => {
|
||||
expect(Roles.admin & Capabilities.ui_access).toBe(Capabilities.ui_access);
|
||||
expect(Roles.admin & Capabilities.owner).toBe(0);
|
||||
expect(Roles.admin & Capabilities.write_users).toBe(Capabilities.write_users);
|
||||
});
|
||||
|
||||
test("auditor has ui_access but limited write permissions", () => {
|
||||
expect(Roles.auditor & Capabilities.ui_access).toBe(Capabilities.ui_access);
|
||||
expect(Roles.auditor & Capabilities.write_users).toBe(0);
|
||||
expect(Roles.auditor & Capabilities.read_users).toBe(Capabilities.read_users);
|
||||
});
|
||||
|
||||
test("member has NO capabilities (including no ui_access)", () => {
|
||||
expect(Roles.member).toBe(0);
|
||||
expect(Roles.member & Capabilities.ui_access).toBe(0);
|
||||
expect(Roles.member & Capabilities.read_machines).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasCapability function", () => {
|
||||
test("returns true when role has the capability", () => {
|
||||
expect(hasCapability("owner", "ui_access")).toBe(true);
|
||||
expect(hasCapability("admin", "ui_access")).toBe(true);
|
||||
expect(hasCapability("auditor", "ui_access")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when role lacks the capability", () => {
|
||||
expect(hasCapability("member", "ui_access")).toBe(false);
|
||||
expect(hasCapability("auditor", "write_users")).toBe(false);
|
||||
});
|
||||
|
||||
test("only owner has owner capability", () => {
|
||||
expect(hasCapability("owner", "owner")).toBe(true);
|
||||
expect(hasCapability("admin", "owner")).toBe(false);
|
||||
expect(hasCapability("member", "owner")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoleFromCapabilities function", () => {
|
||||
test("returns correct role for exact capability match", () => {
|
||||
expect(getRoleFromCapabilities(Roles.owner)).toBe("owner");
|
||||
expect(getRoleFromCapabilities(Roles.admin)).toBe("admin");
|
||||
expect(getRoleFromCapabilities(Roles.auditor)).toBe("auditor");
|
||||
expect(getRoleFromCapabilities(Roles.member)).toBe("member");
|
||||
});
|
||||
|
||||
test("returns member for unrecognized capability values", () => {
|
||||
expect(getRoleFromCapabilities(999999 as any)).toBe("member");
|
||||
});
|
||||
});
|
||||
|
||||
describe("member role", () => {
|
||||
test("blocks UI access", () => {
|
||||
const memberCaps = Roles.member;
|
||||
const hasUIAccess = (memberCaps & Capabilities.ui_access) === Capabilities.ui_access;
|
||||
|
||||
expect(hasUIAccess).toBe(false);
|
||||
expect(memberCaps).toBe(0);
|
||||
});
|
||||
|
||||
test("other roles have UI access", () => {
|
||||
const rolesWithUIAccess = ["owner", "admin", "network_admin", "it_admin", "auditor"] as const;
|
||||
|
||||
for (const role of rolesWithUIAccess) {
|
||||
expect(hasCapability(role, "ui_access")).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import { createClient } from "@libsql/client";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { ulid } from "ulidx";
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { users } from "~/server/db/schema";
|
||||
import { Roles } from "~/server/web/roles";
|
||||
|
||||
// Create in-memory database for testing
|
||||
function createTestDb() {
|
||||
const client = createClient({ url: ":memory:" });
|
||||
const db = drizzle(client);
|
||||
return { client, db };
|
||||
}
|
||||
|
||||
// Create the users table schema
|
||||
async function setupSchema(client: ReturnType<typeof createClient>) {
|
||||
await client.execute(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
sub TEXT NOT NULL UNIQUE,
|
||||
caps INTEGER NOT NULL DEFAULT 0,
|
||||
onboarded INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
describe("Session role assignment", () => {
|
||||
let db: ReturnType<typeof drizzle>;
|
||||
let client: ReturnType<typeof createClient>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const testDb = createTestDb();
|
||||
db = testDb.db;
|
||||
client = testDb.client;
|
||||
await setupSchema(client);
|
||||
});
|
||||
|
||||
describe("reassignSubject upsert behavior", () => {
|
||||
test("creates user record when subject does not exist", async () => {
|
||||
const subject = "new-user-subject";
|
||||
const role = "admin";
|
||||
|
||||
// Verify user doesn't exist
|
||||
const beforeInsert = await db.select().from(users).where(eq(users.sub, subject));
|
||||
expect(beforeInsert.length).toBe(0);
|
||||
|
||||
// Perform upsert (simulating reassignSubject)
|
||||
await db
|
||||
.insert(users)
|
||||
.values({
|
||||
id: ulid(),
|
||||
sub: subject,
|
||||
caps: Roles[role],
|
||||
onboarded: false,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: users.sub,
|
||||
set: { caps: Roles[role] },
|
||||
});
|
||||
|
||||
// Verify user was created with correct role
|
||||
const afterInsert = await db.select().from(users).where(eq(users.sub, subject));
|
||||
expect(afterInsert.length).toBe(1);
|
||||
expect(afterInsert[0].caps).toBe(Roles.admin);
|
||||
});
|
||||
|
||||
test("updates existing user role without creating duplicate", async () => {
|
||||
const subject = "existing-user";
|
||||
const initialRole = "member";
|
||||
const newRole = "admin";
|
||||
|
||||
// Create initial user
|
||||
await db.insert(users).values({
|
||||
id: ulid(),
|
||||
sub: subject,
|
||||
caps: Roles[initialRole],
|
||||
onboarded: true,
|
||||
});
|
||||
|
||||
// Verify initial state
|
||||
const beforeUpdate = await db.select().from(users).where(eq(users.sub, subject));
|
||||
expect(beforeUpdate.length).toBe(1);
|
||||
expect(beforeUpdate[0].caps).toBe(Roles.member);
|
||||
expect(beforeUpdate[0].onboarded).toBe(true);
|
||||
|
||||
// Perform upsert (simulating reassignSubject)
|
||||
await db
|
||||
.insert(users)
|
||||
.values({
|
||||
id: ulid(),
|
||||
sub: subject,
|
||||
caps: Roles[newRole],
|
||||
onboarded: false,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: users.sub,
|
||||
set: { caps: Roles[newRole] },
|
||||
});
|
||||
|
||||
// Verify role was updated, no duplicate created
|
||||
const afterUpdate = await db.select().from(users).where(eq(users.sub, subject));
|
||||
expect(afterUpdate.length).toBe(1);
|
||||
expect(afterUpdate[0].caps).toBe(Roles.admin);
|
||||
// onboarded should remain true (not overwritten)
|
||||
expect(afterUpdate[0].onboarded).toBe(true);
|
||||
});
|
||||
|
||||
test("can assign all role types", async () => {
|
||||
const roles = ["admin", "network_admin", "it_admin", "auditor", "member"] as const;
|
||||
|
||||
for (const role of roles) {
|
||||
const subject = `user-${role}`;
|
||||
|
||||
await db
|
||||
.insert(users)
|
||||
.values({
|
||||
id: ulid(),
|
||||
sub: subject,
|
||||
caps: Roles[role],
|
||||
onboarded: false,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: users.sub,
|
||||
set: { caps: Roles[role] },
|
||||
});
|
||||
|
||||
const [user] = await db.select().from(users).where(eq(users.sub, subject));
|
||||
expect(user.caps).toBe(Roles[role]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("member role handling", () => {
|
||||
test("member role has zero capabilities", async () => {
|
||||
const subject = "member-user";
|
||||
|
||||
await db.insert(users).values({
|
||||
id: ulid(),
|
||||
sub: subject,
|
||||
caps: Roles.member,
|
||||
onboarded: false,
|
||||
});
|
||||
|
||||
const [user] = await db.select().from(users).where(eq(users.sub, subject));
|
||||
expect(user.caps).toBe(0);
|
||||
});
|
||||
|
||||
test("upgrading from member to admin grants ui_access", async () => {
|
||||
const subject = "upgrading-user";
|
||||
|
||||
// Start as member
|
||||
await db.insert(users).values({
|
||||
id: ulid(),
|
||||
sub: subject,
|
||||
caps: Roles.member,
|
||||
onboarded: false,
|
||||
});
|
||||
|
||||
// Upgrade to admin
|
||||
await db
|
||||
.insert(users)
|
||||
.values({
|
||||
id: ulid(),
|
||||
sub: subject,
|
||||
caps: Roles.admin,
|
||||
onboarded: false,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: users.sub,
|
||||
set: { caps: Roles.admin },
|
||||
});
|
||||
|
||||
const [user] = await db.select().from(users).where(eq(users.sub, subject));
|
||||
expect(user.caps).toBe(Roles.admin);
|
||||
expect(user.caps).not.toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user