Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions backend/__tests__/middleware/apiActivityTracker.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"use strict";

// Mock the models before requiring the middleware
jest.mock("../../models/ApiActivity");
jest.mock("../../models/ServiceUsage");

const ApiActivity = require("../../models/ApiActivity");
const { ServiceUsage, TotalUsage } = require("../../models/ServiceUsage");
const apiActivityTracker = require("../../middleware/apiActivityTracker");

describe("apiActivityTracker middleware", () => {
let req;
let res;
let next;
let mockSave;
let mockFindOneAndUpdate;

beforeEach(() => {
req = {
path: "/api/test",
method: "GET",
ip: "127.0.0.1",
user: null,
};
res = {};
next = jest.fn();

mockSave = jest.fn().mockResolvedValue({});
ApiActivity.mockImplementation(() => ({ save: mockSave }));

mockFindOneAndUpdate = jest.fn().mockResolvedValue({});
TotalUsage.findOneAndUpdate = mockFindOneAndUpdate;
ServiceUsage.findOneAndUpdate = mockFindOneAndUpdate;
});

afterEach(() => {
jest.clearAllMocks();
});

it("always calls next() even on success", async () => {
await apiActivityTracker(req, res, next);

expect(next).toHaveBeenCalledTimes(1);
});

it("creates an ApiActivity record with correct fields", async () => {
req.user = { id: "user123" };

await apiActivityTracker(req, res, next);

expect(ApiActivity).toHaveBeenCalledWith({
endpoint: "/api/test",
method: "GET",
userId: "user123",
ipAddress: "127.0.0.1",
});
expect(mockSave).toHaveBeenCalledTimes(1);
});

it("sets userId to null when user is not authenticated", async () => {
req.user = null;

await apiActivityTracker(req, res, next);

expect(ApiActivity).toHaveBeenCalledWith(
expect.objectContaining({ userId: null }),
);
});

it("updates TotalUsage with global key increment", async () => {
await apiActivityTracker(req, res, next);

expect(TotalUsage.findOneAndUpdate).toHaveBeenCalledWith(
{ key: "global" },
{ $inc: { totalCount: 1 }, $setOnInsert: { key: "global" } },
{ upsert: true, new: true },
);
});

it("updates ServiceUsage with endpoint increment", async () => {
await apiActivityTracker(req, res, next);

expect(ServiceUsage.findOneAndUpdate).toHaveBeenCalledWith(
{ endpoint: "/api/test" },
{ $inc: { count: 1 } },
{ upsert: true, new: true },
);
});

it("still calls next() when save throws an error", async () => {
mockSave.mockRejectedValue(new Error("DB error"));
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});

await apiActivityTracker(req, res, next);

expect(next).toHaveBeenCalledTimes(1);
consoleSpy.mockRestore();
});

it("logs an error message when an exception occurs", async () => {
mockSave.mockRejectedValue(new Error("Connection failed"));
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});

await apiActivityTracker(req, res, next);

expect(consoleSpy).toHaveBeenCalledWith(
"Error saving API activity:",
"Connection failed",
);
consoleSpy.mockRestore();
});

it("tracks the correct endpoint path", async () => {
req.path = "/api/convert/png-to-jpg";
req.method = "POST";

await apiActivityTracker(req, res, next);

expect(ApiActivity).toHaveBeenCalledWith(
expect.objectContaining({
endpoint: "/api/convert/png-to-jpg",
method: "POST",
}),
);
expect(ServiceUsage.findOneAndUpdate).toHaveBeenCalledWith(
{ endpoint: "/api/convert/png-to-jpg" },
expect.any(Object),
expect.any(Object),
);
});
});
103 changes: 103 additions & 0 deletions backend/__tests__/middleware/auth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use strict";

const jwt = require("jsonwebtoken");

// Set up test secret before requiring the module
process.env.JWT_SECRET = "test-secret-key";

const authMiddleware = require("../../middleware/auth");

describe("authMiddleware", () => {
let req;
let res;
let next;

beforeEach(() => {
req = { header: jest.fn() };
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
};
next = jest.fn();
});

it("calls next() with no user when no token is provided", () => {
req.header.mockReturnValue(undefined);

authMiddleware(req, res, next);

expect(next).toHaveBeenCalledTimes(1);
expect(req.user).toBeUndefined();
expect(res.status).not.toHaveBeenCalled();
});

it("calls next() and sets req.user when a valid token is provided", () => {
const payload = { user: { id: "abc123", role: "free" } };
const token = jwt.sign(payload, process.env.JWT_SECRET);
req.header.mockReturnValue(token);

authMiddleware(req, res, next);

expect(next).toHaveBeenCalledTimes(1);
expect(req.user).toBeDefined();
expect(req.user.id).toBe("abc123");
expect(req.user.role).toBe("free");
expect(res.status).not.toHaveBeenCalled();
});

it("returns 401 when the token is invalid", () => {
req.header.mockReturnValue("invalid.token.value");

authMiddleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ msg: "Token is not valid" });
expect(next).not.toHaveBeenCalled();
});

it("returns 401 when the token is signed with the wrong secret", () => {
const payload = { user: { id: "abc123", role: "free" } };
const token = jwt.sign(payload, "wrong-secret");
req.header.mockReturnValue(token);

authMiddleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ msg: "Token is not valid" });
expect(next).not.toHaveBeenCalled();
});

it("returns 401 when the token has expired", () => {
const payload = { user: { id: "abc123", role: "free" } };
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: -1 });
req.header.mockReturnValue(token);

authMiddleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ msg: "Token is not valid" });
expect(next).not.toHaveBeenCalled();
});

it("sets req.user.role from token payload", () => {
const payload = { user: { id: "adminId", role: "admin" } };
const token = jwt.sign(payload, process.env.JWT_SECRET);
req.header.mockReturnValue(token);

authMiddleware(req, res, next);

expect(req.user.role).toBe("admin");
expect(next).toHaveBeenCalledTimes(1);
});

it("reads the token from the x-auth-token header", () => {
const payload = { user: { id: "user1", role: "premium" } };
const token = jwt.sign(payload, process.env.JWT_SECRET);
req.header.mockImplementation((name) => (name === "x-auth-token" ? token : undefined));

authMiddleware(req, res, next);

expect(req.header).toHaveBeenCalledWith("x-auth-token");
expect(next).toHaveBeenCalledTimes(1);
});
});
102 changes: 102 additions & 0 deletions backend/__tests__/middleware/authorize.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use strict";

const authorize = require("../../middleware/authorize");

describe("authorize middleware", () => {
let res;
let next;

beforeEach(() => {
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
};
next = jest.fn();
});

it("returns 401 when req.user is not set", () => {
const req = {};
const middleware = authorize(["admin"]);

middleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ msg: "Not authorized" });
expect(next).not.toHaveBeenCalled();
});

it("returns 401 when req.user exists but has no role", () => {
const req = { user: { id: "someId" } };
const middleware = authorize(["admin"]);

middleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ msg: "Not authorized" });
expect(next).not.toHaveBeenCalled();
});

it("returns 403 when user role is not in the allowed roles list", () => {
const req = { user: { id: "userId", role: "free" } };
const middleware = authorize(["admin", "premium"]);

middleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ msg: "Forbidden: Insufficient role" });
expect(next).not.toHaveBeenCalled();
});

it("calls next() when user role is in the allowed roles list", () => {
const req = { user: { id: "adminId", role: "admin" } };
const middleware = authorize(["admin"]);

middleware(req, res, next);

expect(next).toHaveBeenCalledTimes(1);
expect(res.status).not.toHaveBeenCalled();
});

it("calls next() when user role matches one of multiple allowed roles", () => {
const req = { user: { id: "userId", role: "premium" } };
const middleware = authorize(["admin", "premium", "free"]);

middleware(req, res, next);

expect(next).toHaveBeenCalledTimes(1);
expect(res.status).not.toHaveBeenCalled();
});

it("returns 403 when user has admin role but only 'premium' is allowed", () => {
const req = { user: { id: "adminId", role: "admin" } };
const middleware = authorize(["premium"]);

middleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ msg: "Forbidden: Insufficient role" });
expect(next).not.toHaveBeenCalled();
});

it("returns 401 when req.user is null", () => {
const req = { user: null };
const middleware = authorize(["admin"]);

middleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ msg: "Not authorized" });
expect(next).not.toHaveBeenCalled();
});

it("handles empty roles array - denies all users", () => {
const req = { user: { id: "userId", role: "admin" } };
const middleware = authorize([]);

middleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ msg: "Forbidden: Insufficient role" });
expect(next).not.toHaveBeenCalled();
});
});
Loading