Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/middleware/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { PrismaUserRepository } from "../modules/user/repositories/PrismaUserRepository";
import { container, USER_REPOSITORY } from "../shared/infrastructure/container";
import { IUserRepository } from "../modules/user/domain/interfaces/IUserRepository";
import {
AuthenticatedRequest,
DecodedUser,
toAuthenticatedUser,
} from "../types/auth.types";

const SECRET_KEY = process.env.JWT_SECRET || "defaultSecret";
const userRepository = new PrismaUserRepository();
const userRepository = container[USER_REPOSITORY] as IUserRepository;

Comment on lines 11 to 13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Harden JWT handling: no default secret, restrict algorithms, explicit expiry handling.

Using a default secret is unsafe; restrict accepted algs and handle TokenExpiredError distinctly.

-const SECRET_KEY = process.env.JWT_SECRET || "defaultSecret";
-const userRepository = container[USER_REPOSITORY] as IUserRepository;
+const SECRET_KEY = process.env.JWT_SECRET;
+const getUserRepository = (): IUserRepository =>
+  (container[USER_REPOSITORY] as IUserRepository);

 ...
   try {
-    const decoded = jwt.verify(token, SECRET_KEY) as {
+    if (!SECRET_KEY) {
+      res.status(500).json({ message: "Server misconfiguration: JWT secret missing" });
+      return;
+    }
+    const decoded = jwt.verify(token, SECRET_KEY, {
+      algorithms: ["HS256"], // adjust if you use RS256
+    }) as {
       id: string;
       role: string;
     };
-    const user = await userRepository.findById(`${decoded.id}`);
+    const user = await getUserRepository().findById(`${decoded.id}`);
 ...
-  } catch (error) {
+  } catch (error) {
     console.error("Error during authentication:", error);
-    res.status(401).json({ message: "Invalid token" });
+    if (error instanceof (jwt as any).TokenExpiredError) {
+      res.status(401).json({ message: "Token expired" });
+      return;
+    }
+    res.status(401).json({ message: "Invalid token" });
   }

Notes:

  • If you use RS256, swap the algorithm accordingly or make it configurable.

Also applies to: 26-31, 52-56

export const authMiddleware = async (
req: Request,
Expand Down
5 changes: 3 additions & 2 deletions src/modules/auth/presentation/controllers/Auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import {
} from "../../dto/wallet-validation.dto";

// Use cases
import { PrismaUserRepository } from "../../../user/repositories/PrismaUserRepository";
import { container, USER_REPOSITORY } from "../../../shared/infrastructure/container";
import { IUserRepository } from "../../../user/domain/interfaces/IUserRepository";
import { SendVerificationEmailUseCase } from "../../use-cases/send-verification-email.usecase";
import { ResendVerificationEmailUseCase } from "../../use-cases/resend-verification-email.usecase";
import { VerifyEmailUseCase } from "../../use-cases/verify-email.usecase";
import { ValidateWalletFormatUseCase } from "../../use-cases/wallet-format-validation.usecase";
import { VerifyWalletUseCase } from "../../use-cases/verify-wallet.usecase";

const userRepository = new PrismaUserRepository();
const userRepository = container[USER_REPOSITORY] as IUserRepository;
const sendVerificationEmailUseCase = new SendVerificationEmailUseCase(
userRepository
);
Expand Down
238 changes: 173 additions & 65 deletions src/modules/user/__tests__/controllers/UserController.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,21 @@ import request from "supertest";
import express, { Express } from "express";
import UserController from "../../presentation/controllers/UserController";

// Mock the UserService
jest.mock("../../../../services/UserService");
import { UserService } from "../../../../services/UserService";
// Mock the repository
jest.mock("../../../../shared/infrastructure/container", () => ({
container: {
[Symbol.for("USER_REPOSITORY")]: {
create: jest.fn(),
findById: jest.fn(),
findByEmail: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
USER_REPOSITORY: Symbol.for("USER_REPOSITORY"),
}));

import { container, USER_REPOSITORY } from "../../../../shared/infrastructure/container";

// Mock DTOs
jest.mock("../../dto/CreateUserDto", () => {
Expand Down Expand Up @@ -34,14 +46,16 @@ function setupApp(): Express {
app.get("/users/:id", controller.getUserById.bind(controller));
app.get("/users", controller.getUserByEmail.bind(controller));
app.put("/users/:id", controller.updateUser.bind(controller));
app.delete("/users/:id", controller.deleteUser.bind(controller));
return app;
}

const setupMockUserService = (methods: Partial<Record<string, unknown>>) => {
(UserService as jest.Mock).mockImplementation(() => methods);
const setupMockRepository = (methods: Partial<Record<string, unknown>>) => {
const mockRepo = container[USER_REPOSITORY] as any;
Object.assign(mockRepo, methods);
};

describe("UserController", () => {
describe("UserController Integration Tests", () => {
let app: Express;

beforeEach(() => {
Expand All @@ -51,78 +65,134 @@ describe("UserController", () => {

describe("POST /users", () => {
it("should create a user and return 201", async () => {
const mockUser = { id: "1", email: "[email protected]" };
setupMockUserService({
createUser: jest.fn().mockResolvedValue(mockUser),
const mockUser = {
id: "1",
email: "[email protected]",
name: "Test User",
lastName: "Test",
wallet: "GABCDEF123456789",
isVerified: false,
createdAt: new Date(),
updatedAt: new Date()
};

setupMockRepository({
create: jest.fn().mockResolvedValue(mockUser),
});

const res = await request(app)
.post("/users")
.send({ email: "[email protected]" });
.send({
email: "[email protected]",
name: "Test User",
lastName: "Test",
password: "password123",
wallet: "GABCDEF123456789"
});

expect(res.status).toBe(201);
expect(res.body).toEqual(mockUser);
});
Comment on lines 93 to 95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Dates in JSON responses are strings; assertions currently compare to Date objects

Express serializes Date to ISO strings. Loosen assertions to shape + string dates.

-      expect(res.body).toEqual(mockUser);
+      expect(res.body).toEqual(
+        expect.objectContaining({
+          id: mockUser.id,
+          email: mockUser.email,
+          name: mockUser.name,
+          lastName: mockUser.lastName,
+          wallet: mockUser.wallet,
+          isVerified: mockUser.isVerified,
+          createdAt: expect.any(String),
+          updatedAt: expect.any(String),
+        })
+      );
-      expect(res.body).toEqual(mockUser);
+      expect(res.body).toEqual(
+        expect.objectContaining({
+          id: mockUser.id,
+          email: mockUser.email,
+          name: mockUser.name,
+          lastName: mockUser.lastName,
+          wallet: mockUser.wallet,
+          isVerified: mockUser.isVerified,
+          createdAt: expect.any(String),
+          updatedAt: expect.any(String),
+        })
+      );
-      expect(res.body).toEqual(mockUser);
+      expect(res.body).toEqual(
+        expect.objectContaining({
+          id: mockUser.id,
+          email: mockUser.email,
+          name: mockUser.name,
+          lastName: mockUser.lastName,
+          wallet: mockUser.wallet,
+          isVerified: mockUser.isVerified,
+          createdAt: expect.any(String),
+          updatedAt: expect.any(String),
+        })
+      );
-      expect(res.body.user).toEqual(mockUpdatedUser);
+      expect(res.body.user).toEqual(
+        expect.objectContaining({
+          id: mockUpdatedUser.id,
+          email: mockUpdatedUser.email,
+          name: mockUpdatedUser.name,
+          lastName: mockUpdatedUser.lastName,
+          wallet: mockUpdatedUser.wallet,
+          isVerified: mockUpdatedUser.isVerified,
+          createdAt: expect.any(String),
+          updatedAt: expect.any(String),
+        })
+      );

Also applies to: 154-156, 197-199, 236-239

πŸ€– Prompt for AI Agents
In src/modules/user/__tests__/controllers/UserController.int.test.ts around
lines 93-95 (and also update the similar assertions at 154-156, 197-199,
236-239): the test currently compares res.body to mockUser which contains Date
objects, but Express serializes Dates to ISO strings; change the assertions to
verify the response shape and compare date fields as strings (e.g., expect
typeof/res.body properties and
expect(res.body.createdAt).toBe(mockUser.createdAt.toISOString()) or compare the
entire body with a version of mockUser where Date fields are converted to ISO
strings) so tests pass with serialized dates.


it("should handle errors and return 400", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
createUser: jest.fn().mockRejectedValue(new Error("fail")),
}));
it("should handle conflict errors and return 409", async () => {
setupMockRepository({
create: jest.fn().mockRejectedValue(new Error("A user with this email or wallet already exists")),
});

const res = await request(app)
.post("/users")
.send({ email: "[email protected]" });
expect(res.status).toBe(400);
expect(res.body.error).toBe("fail");
.send({
email: "[email protected]",
name: "Test User",
lastName: "Test",
password: "password123",
wallet: "GABCDEF123456789"
});

expect(res.status).toBe(409);
expect(res.body.error).toContain("already exists");
});

it("should handle validation errors with specific status codes", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
createUser: jest
.fn()
.mockRejectedValue(new Error("Invalid email format")),
}));
it("should handle validation errors and return 400", async () => {
setupMockRepository({
create: jest.fn().mockRejectedValue(new Error("Validation error")),
});

const res = await request(app)
.post("/users")
.send({ email: "invalid-email" });
expect(res.status).toBe(422);
expect(res.body.error).toContain("Ivalid email format");
.send({
email: "[email protected]",
name: "Test User",
lastName: "Test",
password: "password123",
wallet: "GABCDEF123456789"
});

expect(res.status).toBe(400);
expect(res.body.error).toContain("validation");
});
});

describe("GET /users/:id", () => {
it("should return a user by id", async () => {
const mockUser = { id: "1", email: "[email protected]" };
(UserService as jest.Mock).mockImplementation(() => ({
getUserById: jest.fn().mockResolvedValue(mockUser),
}));
const mockUser = {
id: "1",
email: "[email protected]",
name: "Test User",
lastName: "Test",
wallet: "GABCDEF123456789",
isVerified: false,
createdAt: new Date(),
updatedAt: new Date()
};

setupMockRepository({
findById: jest.fn().mockResolvedValue(mockUser),
});

const res = await request(app).get("/users/1");
expect(res.status).toBe(200);
expect(res.body).toEqual(mockUser);
});

it("should return 404 if user not found", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
getUserById: jest.fn().mockResolvedValue(null),
}));
setupMockRepository({
findById: jest.fn().mockResolvedValue(null),
});

const res = await request(app).get("/users/1");
expect(res.status).toBe(404);
expect(res.body.error).toBe("User not found");
});

it("should handle errors and return 400", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
getUserById: jest.fn().mockRejectedValue(new Error("fail")),
}));
it("should handle errors and return 500", async () => {
setupMockRepository({
findById: jest.fn().mockRejectedValue(new Error("Database error")),
});

const res = await request(app).get("/users/1");
expect(res.status).toBe(400);
expect(res.body.error).toBe("fail");
expect(res.status).toBe(500);
expect(res.body.error).toBe("Internal server error");
});
});

describe("GET /users?email=", () => {
it("should return a user by email", async () => {
const mockUser = { id: "1", email: "[email protected]" };
(UserService as jest.Mock).mockImplementation(() => ({
getUserByEmail: jest.fn().mockResolvedValue(mockUser),
}));
const mockUser = {
id: "1",
email: "[email protected]",
name: "Test User",
lastName: "Test",
wallet: "GABCDEF123456789",
isVerified: false,
createdAt: new Date(),
updatedAt: new Date()
};

setupMockRepository({
findByEmail: jest.fn().mockResolvedValue(mockUser),
});

const res = await request(app).get("/[email protected]");
expect(res.status).toBe(200);
expect(res.body).toEqual(mockUser);
Expand All @@ -135,41 +205,79 @@ describe("UserController", () => {
});

it("should return 404 if user not found", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
getUserByEmail: jest.fn().mockResolvedValue(null),
}));
setupMockRepository({
findByEmail: jest.fn().mockResolvedValue(null),
});

const res = await request(app).get("/[email protected]");
expect(res.status).toBe(404);
expect(res.body.error).toBe("User not found");
});

it("should handle errors and return 400", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
getUserByEmail: jest.fn().mockRejectedValue(new Error("fail")),
}));
const res = await request(app).get("/[email protected]");
expect(res.status).toBe(400);
expect(res.body.error).toBe("fail");
});
});

describe("PUT /users/:id", () => {
it("should update a user and return 200", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
updateUser: jest.fn().mockResolvedValue(undefined),
}));
const res = await request(app).put("/users/1").send({ name: "Updated" });
const mockUpdatedUser = {
id: "1",
email: "[email protected]",
name: "Updated User",
lastName: "Updated",
wallet: "GABCDEF123456789",
isVerified: false,
createdAt: new Date(),
updatedAt: new Date()
};

setupMockRepository({
update: jest.fn().mockResolvedValue(mockUpdatedUser),
});

const res = await request(app).put("/users/1").send({ name: "Updated User" });
expect(res.status).toBe(200);
expect(res.body.message).toBe("User updated successfully");
expect(res.body.user).toEqual(mockUpdatedUser);
});

it("should handle errors and return 400", async () => {
(UserService as jest.Mock).mockImplementation(() => ({
updateUser: jest.fn().mockRejectedValue(new Error("fail")),
}));
it("should handle not found errors and return 404", async () => {
setupMockRepository({
update: jest.fn().mockRejectedValue(new Error("User not found")),
});

const res = await request(app).put("/users/1").send({ name: "Updated" });
expect(res.status).toBe(400);
expect(res.body.error).toBe("fail");
expect(res.status).toBe(404);
expect(res.body.error).toBe("User not found");
});

it("should handle conflict errors and return 409", async () => {
setupMockRepository({
update: jest.fn().mockRejectedValue(new Error("A user with this email already exists")),
});

const res = await request(app).put("/users/1").send({ email: "[email protected]" });
expect(res.status).toBe(409);
expect(res.body.error).toBe("A user with this email already exists");
});
});

describe("DELETE /users/:id", () => {
it("should delete a user and return 200", async () => {
setupMockRepository({
delete: jest.fn().mockResolvedValue(undefined),
});

const res = await request(app).delete("/users/1");
expect(res.status).toBe(200);
expect(res.body.message).toBe("User deleted successfully");
});

it("should handle not found errors and return 404", async () => {
setupMockRepository({
delete: jest.fn().mockRejectedValue(new Error("User not found")),
});

const res = await request(app).delete("/users/1");
expect(res.status).toBe(404);
expect(res.body.error).toBe("User not found");
});
});
});
Loading