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
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:
parent
5ef2026b61
commit
1db50e41f9
5 changed files with 336 additions and 2 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue