mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-04-18 05:47:31 -04:00
* chore: migrate to vitest * test: speed up some suites by sharing sessions and mocking expensive non-tested actions * test: refactor some tests to verify behavior instead of implementation details * chore: fix linting issues
229 lines
8.3 KiB
TypeScript
229 lines
8.3 KiB
TypeScript
import { beforeAll, describe, expect, test } from "vitest";
|
|
import { createApp } from "~/server/app";
|
|
import {
|
|
createTestSession,
|
|
createTestSessionWithGlobalAdmin,
|
|
createTestSessionWithOrgAdmin,
|
|
createTestSessionWithRegularMember,
|
|
getAuthHeaders,
|
|
} from "~/test/helpers/auth";
|
|
import { account } from "~/server/db/schema";
|
|
import { db } from "~/server/db/db";
|
|
|
|
const app = createApp();
|
|
|
|
describe("auth controller security", () => {
|
|
let regularUserSession: Awaited<ReturnType<typeof createTestSession>>;
|
|
let regularMemberSession: Awaited<ReturnType<typeof createTestSessionWithRegularMember>>;
|
|
let orgAdminSession: Awaited<ReturnType<typeof createTestSessionWithOrgAdmin>>;
|
|
let globalAdminSession: Awaited<ReturnType<typeof createTestSessionWithGlobalAdmin>>;
|
|
|
|
beforeAll(async () => {
|
|
regularUserSession = await createTestSession();
|
|
regularMemberSession = await createTestSessionWithRegularMember();
|
|
orgAdminSession = await createTestSessionWithOrgAdmin();
|
|
globalAdminSession = await createTestSessionWithGlobalAdmin();
|
|
});
|
|
|
|
describe("public endpoints - no auth required", () => {
|
|
test("GET /api/v1/auth/status should be accessible without authentication", async () => {
|
|
const res = await app.request("/api/v1/auth/status");
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
test("GET /api/v1/auth/sso-providers should be accessible without authentication", async () => {
|
|
const res = await app.request("/api/v1/auth/sso-providers");
|
|
expect(res.status).toBe(200);
|
|
});
|
|
|
|
test("GET /api/v1/auth/login-error should be accessible without authentication", async () => {
|
|
const res = await app.request("/api/v1/auth/login-error?error=test");
|
|
expect(res.status).toBe(302);
|
|
});
|
|
});
|
|
|
|
describe("org admin endpoints - require requireAuth + requireOrgAdmin", () => {
|
|
const orgAdminEndpoints = [
|
|
{ method: "GET", path: "/api/v1/auth/sso-settings" },
|
|
{ method: "DELETE", path: "/api/v1/auth/sso-providers/test-provider" },
|
|
{ method: "PATCH", path: "/api/v1/auth/sso-providers/test-provider/auto-linking" },
|
|
{ method: "DELETE", path: "/api/v1/auth/sso-invitations/test-invitation" },
|
|
{ method: "GET", path: "/api/v1/auth/org-members" },
|
|
{ method: "PATCH", path: "/api/v1/auth/org-members/test-member/role" },
|
|
{ method: "DELETE", path: "/api/v1/auth/org-members/test-member" },
|
|
];
|
|
|
|
for (const { method, path } of orgAdminEndpoints) {
|
|
test(`${method} ${path} should return 401 when unauthenticated`, async () => {
|
|
const res = await app.request(path, { method });
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Invalid or expired session");
|
|
});
|
|
|
|
test(`${method} ${path} should return 403 for regular members`, async () => {
|
|
const res = await app.request(path, {
|
|
method,
|
|
headers: regularMemberSession.headers,
|
|
body: method !== "GET" && method !== "DELETE" ? JSON.stringify({}) : undefined,
|
|
});
|
|
expect(res.status).toBe(403);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Forbidden");
|
|
});
|
|
|
|
test(`${method} ${path} should be accessible to org admins`, async () => {
|
|
const res = await app.request(path, {
|
|
method,
|
|
headers: orgAdminSession.headers,
|
|
body: method !== "GET" && method !== "DELETE" ? JSON.stringify({}) : undefined,
|
|
});
|
|
// Should not be 401 or 403 - actual response depends on endpoint logic
|
|
expect(res.status).not.toBe(401);
|
|
expect(res.status).not.toBe(403);
|
|
});
|
|
}
|
|
|
|
describe("PATCH /api/v1/auth/sso-providers/:providerId/auto-linking specific", () => {
|
|
test("should return 400 for invalid payload", async () => {
|
|
const res = await app.request("/api/v1/auth/sso-providers/test-provider/auto-linking", {
|
|
method: "PATCH",
|
|
headers: {
|
|
...orgAdminSession.headers,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe("PATCH /api/v1/auth/org-members/:memberId/role specific", () => {
|
|
test("should return 400 for invalid payload", async () => {
|
|
const res = await app.request("/api/v1/auth/org-members/test-member/role", {
|
|
method: "PATCH",
|
|
headers: {
|
|
...orgAdminSession.headers,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("global admin endpoints - require requireAuth + requireAdmin", () => {
|
|
const adminEndpoints = [
|
|
{ method: "GET", path: "/api/v1/auth/admin-users" },
|
|
{ method: "DELETE", path: "/api/v1/auth/admin-users/test-user/accounts/test-account" },
|
|
{ method: "GET", path: "/api/v1/auth/deletion-impact/test-user" },
|
|
];
|
|
|
|
for (const { method, path } of adminEndpoints) {
|
|
test(`${method} ${path} should return 401 when unauthenticated`, async () => {
|
|
const res = await app.request(path, { method });
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Invalid or expired session");
|
|
});
|
|
|
|
test(`${method} ${path} should return 403 for regular users`, async () => {
|
|
const res = await app.request(path, { method, headers: regularUserSession.headers });
|
|
expect(res.status).toBe(403);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Forbidden");
|
|
});
|
|
|
|
test(`${method} ${path} should return 403 for org admins`, async () => {
|
|
const res = await app.request(path, { method, headers: orgAdminSession.headers });
|
|
expect(res.status).toBe(403);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Forbidden");
|
|
});
|
|
|
|
test(`${method} ${path} should not return 401 for global admins`, async () => {
|
|
const res = await app.request(path, { method, headers: globalAdminSession.headers });
|
|
// Should not be 401 - actual response depends on endpoint logic
|
|
expect(res.status).not.toBe(401);
|
|
});
|
|
}
|
|
|
|
test("global admins can delete an account for a user outside their active organization", async () => {
|
|
const target = await createTestSession();
|
|
|
|
const retainedAccountId = Bun.randomUUIDv7();
|
|
await db.insert(account).values({
|
|
id: retainedAccountId,
|
|
accountId: `credential-${retainedAccountId}`,
|
|
providerId: "credential",
|
|
userId: target.user.id,
|
|
password: "password-hash",
|
|
});
|
|
|
|
const removableAccountId = Bun.randomUUIDv7();
|
|
await db.insert(account).values({
|
|
id: removableAccountId,
|
|
accountId: `oidc-${removableAccountId}`,
|
|
providerId: "oidc-acme",
|
|
userId: target.user.id,
|
|
});
|
|
|
|
const res = await app.request(`/api/v1/auth/admin-users/${target.user.id}/accounts/${removableAccountId}`, {
|
|
method: "DELETE",
|
|
headers: globalAdminSession.headers,
|
|
});
|
|
|
|
expect(res.status).toBe(200);
|
|
|
|
const deletedAccount = await db.query.account.findFirst({
|
|
where: { id: removableAccountId },
|
|
columns: { id: true },
|
|
});
|
|
|
|
expect(deletedAccount).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("invalid session handling", () => {
|
|
test("should return 401 for invalid session cookie", async () => {
|
|
const res = await app.request("/api/v1/auth/sso-settings", {
|
|
headers: getAuthHeaders("invalid-session-token"),
|
|
});
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Invalid or expired session");
|
|
});
|
|
|
|
test("should return 401 when session cookie is missing", async () => {
|
|
const res = await app.request("/api/v1/auth/admin-users");
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Invalid or expired session");
|
|
});
|
|
});
|
|
|
|
describe("information disclosure", () => {
|
|
test("should not disclose org members when unauthenticated", async () => {
|
|
const res = await app.request("/api/v1/auth/org-members");
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Invalid or expired session");
|
|
});
|
|
|
|
test("should not disclose SSO settings when unauthenticated", async () => {
|
|
const res = await app.request("/api/v1/auth/sso-settings");
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Invalid or expired session");
|
|
});
|
|
|
|
test("should not disclose admin users when unauthenticated", async () => {
|
|
const res = await app.request("/api/v1/auth/admin-users");
|
|
expect(res.status).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message).toBe("Invalid or expired session");
|
|
});
|
|
});
|
|
});
|