add isAdmin to secure specific endpoints

This commit is contained in:
StarAppeal
2025-09-15 20:54:19 +02:00
parent 6a9ffde9f6
commit da19a0aaf1
5 changed files with 229 additions and 62 deletions
+95
View File
@@ -0,0 +1,95 @@
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest";
import { Request, Response, NextFunction } from "express";
import { isAdmin } from "../../../src/rest/middleware/isAdmin";
import { createMockUserService } from "../../helpers/testSetup";
import { UserService } from "../../../src/db/services/db/UserService";
import { notFound } from "../../../src/rest/utils/responses";
vi.mock("../../../src/db/services/db/UserService", () => ({
UserService: {
create: vi.fn(),
},
}));
vi.mock("../../../src/rest/utils/responses", () => ({
notFound: vi.fn(),
}));
describe("isAdmin middleware", () => {
let mockedUserService: any;
let req: Partial<Request>;
let res: Mocked<Response>;
let next: Mocked<NextFunction>;
const uuid = "abcd-coffe";
beforeEach(() => {
vi.clearAllMocks();
mockedUserService = createMockUserService();
vi.mocked(UserService.create).mockResolvedValue(mockedUserService);
req = {
payload: { uuid, username: "username", id: ""}
};
res = {
status: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
} as unknown as Mocked<Response>;
next = vi.fn();
});
afterEach(() => {
vi.resetAllMocks();
});
describe("Success Scenarios", () => {
it("should call next() if user is an admin", async () => {
const mockUser = { uuid, config: { isAdmin: true } };
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
await isAdmin(req as Request, res, next);
expect(UserService.create).toHaveBeenCalledOnce();
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(next).toHaveBeenCalledOnce();
expect(res.status).not.toHaveBeenCalled();
expect(res.send).not.toHaveBeenCalled();
expect(notFound).not.toHaveBeenCalled();
});
});
describe("Failure Scenarios", () => {
it("should call notFound if user is not an admin", async () => {
const mockUser = { uuid, config: { isAdmin: false } };
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
await isAdmin(req as Request, res, next);
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(notFound).toHaveBeenCalledWith(res);
expect(next).not.toHaveBeenCalled();
});
it("should call notFound if user does not exist", async () => {
mockedUserService.getUserByUUID.mockResolvedValue(null);
await isAdmin(req as Request, res, next);
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(notFound).toHaveBeenCalledWith(res);
expect(next).not.toHaveBeenCalled();
});
it("should handle errors during user fetching", async () => {
const dbError = new Error("Database error");
mockedUserService.getUserByUUID.mockRejectedValue(dbError);
await isAdmin(req as Request, res, next);
expect(next).toHaveBeenCalledWith(expect.objectContaining({ message: 'Database error' }));
});
});
});
+107 -60
View File
@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest";
import request from "supertest";
import { RestUser } from "../../src/rest/restUser";
import { setupTestEnvironment, type TestEnvironment } from "../helpers/testSetup";
import {RestUser} from "../../src/rest/restUser";
import {setupTestEnvironment, type TestEnvironment} from "../helpers/testSetup";
vi.mock("../../src/db/services/db/UserService", () => ({
UserService: {
@@ -21,6 +21,10 @@ vi.mock("../../src/utils/passwordUtils", () => ({
describe("RestUser", () => {
let testEnv: TestEnvironment;
const requestingUserUUID = "test-user-uuid";
const adminUser = { uuid: requestingUserUUID, config: { isAdmin: true } };
const nonAdminUser = { uuid: requestingUserUUID, config: { isAdmin: false } };
beforeEach(() => {
vi.clearAllMocks();
@@ -32,35 +36,6 @@ describe("RestUser", () => {
vi.resetAllMocks();
});
describe("GET /", () => {
it("should return all users", async () => {
const mockUsers = [
{id: "1", name: "user1", uuid: "uuid1"},
{id: "2", name: "user2", uuid: "uuid2"}
];
testEnv.mockUserService.getAllUsers.mockResolvedValue(mockUsers);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual(mockUsers);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
});
it("should handle empty user list", async () => {
testEnv. mockUserService.getAllUsers.mockResolvedValue([]);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual([]);
});
});
describe("GET /me", () => {
it("should return current user", async () => {
const mockUser = {
@@ -118,7 +93,7 @@ describe("RestUser", () => {
});
it("should return bad request when user not found", async () => {
testEnv. mockUserService.getUserByUUID.mockResolvedValue(null);
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.put("/user/me/spotify")
@@ -240,7 +215,7 @@ describe("RestUser", () => {
password: "old-hashed-password"
};
testEnv. mockUserService.getUserByUUID.mockResolvedValue(mockUser);
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
vi.mocked(PasswordUtils.validatePassword).mockReturnValue({valid: true});
vi.mocked(PasswordUtils.hashPassword).mockResolvedValue("new-hashed-password");
testEnv.mockUserService.updateUser.mockResolvedValue(mockUser);
@@ -352,40 +327,112 @@ describe("RestUser", () => {
});
});
describe("GET /:id", () => {
it("should return user by id", async () => {
const mockUser = {
id: "specific-user-id",
name: "specificuser",
uuid: "specific-uuid"
};
describe("GET / (Admin only)", () => {
testEnv.mockUserService.getUserById.mockResolvedValue(mockUser);
describe("when user is an admin", () => {
beforeEach(() => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(adminUser);
});
const response = await request(testEnv.app)
.get("/user/specific-user-id")
.expect(200);
it("should return all users", async () => {
const mockUsers = [
{id: "1", name: "user1", uuid: "uuid1"},
{id: "2", name: "user2", uuid: "uuid2"}
];
testEnv.mockUserService.getAllUsers.mockResolvedValue(mockUsers);
expect(response.body.data).toEqual(mockUser);
expect(testEnv.mockUserService.getUserById).toHaveBeenCalledWith("specific-user-id");
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual(mockUsers);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
});
it("should handle empty user list", async () => {
testEnv.mockUserService.getAllUsers.mockResolvedValue([]);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual([]);
});
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(null);
describe("when user is not an admin", () => {
it("should return 404 Not Found if user is not an admin", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
const response = await request(testEnv.app)
.get("/user/nonexistent-id")
.expect(400);
await request(testEnv.app)
.get("/user/")
.expect(404);
});
expect(response.body.data.message).toBe("Unable to find matching document with id: nonexistent-id");
});
it("should return 404 Not Found if user does not exist", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
it("should return all users when id is empty", async () => {
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
await request(testEnv.app)
.get("/user/")
.expect(404);
});
});
});
describe("GET /:id (Admin only)", () => {
const specificUserId = "specific-user-id";
const mockUser = {
id: specificUserId,
name: "specificuser",
uuid: "specific-uuid"
};
describe("when user is an admin", () => {
beforeEach(() => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(adminUser);
});
it("should return user by id", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.get(`/user/${specificUserId}`)
.expect(200);
expect(response.body.data).toEqual(mockUser);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(testEnv.mockUserService.getUserById).toHaveBeenCalledWith(specificUserId);
});
it("should return bad request when target user is not found", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(null);
const response = await request(testEnv.app)
.get(`/user/nonexistent-id`)
.expect(400);
expect(response.body.data.message).toBe("Unable to find matching document with id: nonexistent-id");
});
});
describe("when user is not an admin", () => {
it("should return 404 Not Found if user is not an admin", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
await request(testEnv.app)
.get(`/user/${specificUserId}`)
.expect(404);
});
it("should return 404 Not Found if user does not exist", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
await request(testEnv.app)
.get(`/user/${specificUserId}`)
.expect(404);
});
});
});
});