test: increase coverage for existing controllers
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled

This commit is contained in:
Nicolas Meienberger 2026-03-02 21:16:48 +01:00
parent 5ef2026b61
commit 1db50e41f9
5 changed files with 336 additions and 2 deletions

View file

@ -0,0 +1,197 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { createApp } from "~/server/app";
import {
createTestSession,
createTestSessionWithGlobalAdmin,
createTestSessionWithOrgAdmin,
createTestSessionWithRegularMember,
getAuthHeaders,
} from "~/test/helpers/auth";
import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema";
import { db } from "~/server/db/db";
const app = createApp();
describe("auth controller security", () => {
beforeEach(async () => {
await db.delete(member);
await db.delete(account);
await db.delete(invitation);
await db.delete(ssoProvider);
await db.delete(organization);
await db.delete(usersTable);
});
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 { headers } = await createTestSessionWithRegularMember();
const res = await app.request(path, {
method,
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 { headers } = await createTestSessionWithOrgAdmin();
const res = await app.request(path, {
method,
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 { headers } = await createTestSessionWithOrgAdmin();
const res = await app.request("/api/v1/auth/sso-providers/test-provider/auto-linking", {
method: "PATCH",
headers: {
...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 { headers } = await createTestSessionWithOrgAdmin();
const res = await app.request("/api/v1/auth/org-members/test-member/role", {
method: "PATCH",
headers: {
...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 { headers } = await createTestSession();
const res = await app.request(path, { method, 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 { headers } = await createTestSessionWithOrgAdmin();
const res = await app.request(path, { method, 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 { headers } = await createTestSessionWithGlobalAdmin();
const res = await app.request(path, { method, headers });
// Should not be 401 - actual response depends on endpoint logic
expect(res.status).not.toBe(401);
});
}
});
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");
});
});
});

View file

@ -51,6 +51,7 @@ describe("backups security", () => {
{ method: "PUT", path: "/api/v1/backups/1/mirrors" },
{ method: "GET", path: "/api/v1/backups/1/mirrors/compatibility" },
{ method: "POST", path: "/api/v1/backups/reorder" },
{ method: "GET", path: "/api/v1/backups/1/progress" },
];
for (const { method, path } of endpoints) {

View file

@ -75,13 +75,17 @@ describe("repositories security", () => {
{ method: "GET", path: "/api/v1/repositories/test-repo/stats" },
{ method: "DELETE", path: "/api/v1/repositories/test-repo" },
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots" },
{ method: "POST", path: "/api/v1/repositories/test-repo/snapshots/refresh" },
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/files" },
{ method: "GET", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot/dump" },
{ method: "POST", path: "/api/v1/repositories/test-repo/restore" },
{ method: "POST", path: "/api/v1/repositories/test-repo/doctor" },
{ method: "DELETE", path: "/api/v1/repositories/test-repo/doctor" },
{ method: "POST", path: "/api/v1/repositories/test-repo/unlock" },
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots/test-snapshot" },
{ method: "DELETE", path: "/api/v1/repositories/test-repo/snapshots" },
{ method: "POST", path: "/api/v1/repositories/test-repo/snapshots/tag" },
{ method: "PATCH", path: "/api/v1/repositories/test-repo" },
];
@ -95,6 +99,21 @@ describe("repositories security", () => {
}
});
describe("dev panel endpoint - requires dev panel access", () => {
test("POST /api/v1/repositories/:shortId/exec should return 401 when unauthenticated", async () => {
const res = await app.request("/api/v1/repositories/test-repo/exec", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ command: "version" }),
});
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 if a repository exists when unauthenticated", async () => {
const res = await app.request("/api/v1/repositories/non-existent-repo");

View file

@ -1,6 +1,6 @@
import { test, describe, expect } from "bun:test";
import { createApp } from "~/server/app";
import { createTestSession, getAuthHeaders } from "~/test/helpers/auth";
import { createTestSession, createTestSessionWithGlobalAdmin, getAuthHeaders } from "~/test/helpers/auth";
const app = createApp();
@ -34,7 +34,11 @@ describe("system security", () => {
describe("unauthenticated access", () => {
const endpoints: { method: string; path: string }[] = [
{ method: "GET", path: "/api/v1/system/info" },
{ method: "GET", path: "/api/v1/system/updates" },
{ method: "GET", path: "/api/v1/system/registration-status" },
{ method: "PUT", path: "/api/v1/system/registration-status" },
{ method: "POST", path: "/api/v1/system/restic-password" },
{ method: "GET", path: "/api/v1/system/dev-panel" },
];
for (const { method, path } of endpoints) {
@ -47,6 +51,62 @@ describe("system security", () => {
}
});
describe("registration-status endpoint", () => {
test("GET /api/v1/system/registration-status should be accessible with valid session", async () => {
const { headers } = await createTestSession();
const res = await app.request("/api/v1/system/registration-status", { headers });
expect(res.status).toBe(200);
const body = await res.json();
expect(typeof body.enabled).toBe("boolean");
});
test("PUT /api/v1/system/registration-status should return 403 for non-admin users", async () => {
const { headers } = await createTestSession();
const res = await app.request("/api/v1/system/registration-status", {
method: "PUT",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: false }),
});
expect(res.status).toBe(403);
const body = await res.json();
expect(body.message).toBe("Forbidden");
});
test("PUT /api/v1/system/registration-status should be accessible to global admin", async () => {
const { headers } = await createTestSessionWithGlobalAdmin();
const res = await app.request("/api/v1/system/registration-status", {
method: "PUT",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: false }),
});
expect(res.status).toBe(200);
});
});
describe("dev-panel endpoint", () => {
test("GET /api/v1/system/dev-panel should be accessible with valid session", async () => {
const { headers } = await createTestSession();
const res = await app.request("/api/v1/system/dev-panel", { headers });
expect(res.status).toBe(200);
const body = await res.json();
expect(typeof body.enabled).toBe("boolean");
});
});
describe("updates endpoint", () => {
test("GET /api/v1/system/updates should be accessible with valid session", async () => {
const { headers } = await createTestSession();
const res = await app.request("/api/v1/system/updates", { headers });
expect(res.status).toBe(200);
});
});
describe("input validation", () => {
test("should return 400 for invalid payload on restic-password", async () => {
const { headers } = await createTestSession();

View file

@ -1,4 +1,7 @@
import { auth } from "~/server/lib/auth";
import { db } from "~/server/db/db";
import { member, organization, sessionsTable, usersTable } from "~/server/db/schema";
import { eq } from "drizzle-orm";
export const COOKIE_PREFIX = "zerobyte";
@ -12,6 +15,12 @@ export async function createTestSession() {
const ctx = await auth.$context;
const user = ctx.test.createUser();
await ctx.test.saveUser(user);
const allUsers = await db.query.usersTable.findMany();
if (allUsers.length === 1 && allUsers[0].role === "admin") {
await db.update(usersTable).set({ role: "user" }).where(eq(usersTable.id, user.id));
}
const { headers, session } = await ctx.test.login({ userId: user.id });
const organizationId = (session as { activeOrganizationId?: string }).activeOrganizationId ?? "";
@ -19,7 +28,55 @@ export async function createTestSession() {
return {
headers: Object.fromEntries(headers.entries()) as Record<string, string>,
session,
user,
user: { ...user, role: "user" },
organizationId,
};
}
export async function createTestSessionWithOrgAdmin() {
const { headers, user, organizationId } = await createTestSession();
await db.update(member).set({ role: "admin" }).where(eq(member.userId, user.id));
return { headers, user, organizationId };
}
export async function createTestSessionWithGlobalAdmin() {
const ctx = await auth.$context;
const user = ctx.test.createUser();
await ctx.test.saveUser(user);
await db.update(usersTable).set({ role: "admin" }).where(eq(usersTable.id, user.id));
const [org] = await db
.insert(organization)
.values({ id: crypto.randomUUID(), name: "Admin Org", slug: `admin-org-${Date.now()}`, createdAt: new Date() })
.returning();
await db.insert(member).values({
id: crypto.randomUUID(),
organizationId: org.id,
userId: user.id,
role: "owner",
createdAt: new Date(),
});
await db.update(sessionsTable).set({ activeOrganizationId: org.id }).where(eq(sessionsTable.userId, user.id));
const { headers, session } = await ctx.test.login({ userId: user.id });
return {
headers: Object.fromEntries(headers.entries()) as Record<string, string>,
session,
user,
organizationId: org.id,
};
}
export async function createTestSessionWithRegularMember() {
const { headers, user, organizationId } = await createTestSession();
await db.update(member).set({ role: "member" }).where(eq(member.userId, user.id));
return { headers, user, organizationId };
}