Merge pull request #469 from drifterza/feature/oidc-pending-approval

This commit is contained in:
Aarnav Tale
2026-02-26 18:03:26 -05:00
committed by GitHub
8 changed files with 905 additions and 473 deletions
+94
View File
@@ -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();
});
});
+81
View File
@@ -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);
}
});
});
});
+180
View File
@@ -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);
});
});
});