diff --git a/backend/src/users/repositories/users.repository.spec.ts b/backend/src/users/repositories/users.repository.spec.ts new file mode 100644 index 0000000..41df83a --- /dev/null +++ b/backend/src/users/repositories/users.repository.spec.ts @@ -0,0 +1,150 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { DatabaseService } from "../../database/database.service"; +import { UsersRepository } from "./users.repository"; + +describe("UsersRepository", () => { + let repository: UsersRepository; + let _databaseService: DatabaseService; + + const mockDb = { + insert: jest.fn().mockReturnThis(), + values: jest.fn().mockReturnThis(), + returning: jest.fn().mockResolvedValue([{ uuid: "u1" }]), + select: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + update: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + delete: jest.fn().mockReturnThis(), + transaction: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UsersRepository, + { + provide: DatabaseService, + useValue: { db: mockDb }, + }, + ], + }).compile(); + + repository = module.get(UsersRepository); + _databaseService = module.get(DatabaseService); + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(repository).toBeDefined(); + }); + + describe("create", () => { + it("should insert a user", async () => { + const data = { + username: "u", + email: "e", + passwordHash: "p", + emailHash: "eh", + }; + await repository.create(data); + expect(mockDb.insert).toHaveBeenCalled(); + expect(mockDb.values).toHaveBeenCalledWith(data); + }); + }); + + describe("findByEmailHash", () => { + it("should select user by email hash", async () => { + mockDb.limit.mockResolvedValueOnce([{ uuid: "u1" }]); + const result = await repository.findByEmailHash("hash"); + expect(result.uuid).toBe("u1"); + expect(mockDb.select).toHaveBeenCalled(); + expect(mockDb.where).toHaveBeenCalled(); + }); + }); + + describe("findOneWithPrivateData", () => { + it("should select user with private data", async () => { + mockDb.limit.mockResolvedValueOnce([{ uuid: "u1" }]); + const result = await repository.findOneWithPrivateData("u1"); + expect(result.uuid).toBe("u1"); + }); + }); + + describe("countAll", () => { + it("should return count", async () => { + mockDb.from.mockResolvedValueOnce([{ count: 5 }]); + const result = await repository.countAll(); + expect(result).toBe(5); + }); + }); + + describe("findAll", () => { + it("should select users with limit and offset", async () => { + mockDb.offset.mockResolvedValueOnce([{ uuid: "u1" }]); + const result = await repository.findAll(10, 0); + expect(result[0].uuid).toBe("u1"); + expect(mockDb.limit).toHaveBeenCalledWith(10); + expect(mockDb.offset).toHaveBeenCalledWith(0); + }); + }); + + describe("findByUsername", () => { + it("should find by username", async () => { + mockDb.limit.mockResolvedValueOnce([{ uuid: "u1" }]); + const result = await repository.findByUsername("u"); + expect(result.uuid).toBe("u1"); + }); + }); + + describe("update", () => { + it("should update user", async () => { + mockDb.returning.mockResolvedValueOnce([{ uuid: "u1" }]); + await repository.update("u1", { displayName: "New" }); + expect(mockDb.update).toHaveBeenCalled(); + expect(mockDb.set).toHaveBeenCalled(); + }); + }); + + describe("getTwoFactorSecret", () => { + it("should return secret", async () => { + mockDb.limit.mockResolvedValueOnce([{ secret: "s" }]); + const result = await repository.getTwoFactorSecret("u1"); + expect(result).toBe("s"); + }); + }); + + describe("getUserContents", () => { + it("should return contents", async () => { + mockDb.where.mockResolvedValueOnce([{ id: "c1" }]); + const result = await repository.getUserContents("u1"); + expect(result[0].id).toBe("c1"); + }); + }); + + describe("softDeleteUserAndContents", () => { + it("should run transaction", async () => { + const mockTx = { + update: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + returning: jest.fn().mockResolvedValue([{ uuid: "u1" }]), + }; + mockDb.transaction.mockImplementation(async (cb) => cb(mockTx)); + + const result = await repository.softDeleteUserAndContents("u1"); + expect(result[0].uuid).toBe("u1"); + expect(mockTx.update).toHaveBeenCalledTimes(2); + }); + }); + + describe("purgeDeleted", () => { + it("should delete old deleted users", async () => { + mockDb.returning.mockResolvedValueOnce([{ uuid: "u1" }]); + const _result = await repository.purgeDeleted(new Date()); + expect(mockDb.delete).toHaveBeenCalled(); + }); + }); +}); diff --git a/backend/src/users/users.controller.spec.ts b/backend/src/users/users.controller.spec.ts new file mode 100644 index 0000000..30169d5 --- /dev/null +++ b/backend/src/users/users.controller.spec.ts @@ -0,0 +1,192 @@ +jest.mock("uuid", () => ({ + v4: jest.fn(() => "mocked-uuid"), +})); + +jest.mock("@noble/post-quantum/ml-kem.js", () => ({ + ml_kem768: { + keygen: jest.fn(), + encapsulate: jest.fn(), + decapsulate: jest.fn(), + }, +})); + +jest.mock("jose", () => ({ + SignJWT: jest.fn().mockReturnValue({ + setProtectedHeader: jest.fn().mockReturnThis(), + setIssuedAt: jest.fn().mockReturnThis(), + setExpirationTime: jest.fn().mockReturnThis(), + sign: jest.fn().mockResolvedValue("mocked-jwt"), + }), + jwtVerify: jest.fn(), +})); + +import { CACHE_MANAGER } from "@nestjs/cache-manager"; +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthService } from "../auth/auth.service"; +import { AuthGuard } from "../auth/guards/auth.guard"; +import { RolesGuard } from "../auth/guards/roles.guard"; +import { AuthenticatedRequest } from "../common/interfaces/request.interface"; +import { UsersController } from "./users.controller"; +import { UsersService } from "./users.service"; + +describe("UsersController", () => { + let controller: UsersController; + let usersService: UsersService; + let authService: AuthService; + + const mockUsersService = { + findAll: jest.fn(), + findPublicProfile: jest.fn(), + findOneWithPrivateData: jest.fn(), + exportUserData: jest.fn(), + update: jest.fn(), + updateAvatar: jest.fn(), + updateConsent: jest.fn(), + remove: jest.fn(), + }; + + const mockAuthService = { + generateTwoFactorSecret: jest.fn(), + enableTwoFactor: jest.fn(), + disableTwoFactor: jest.fn(), + }; + + const mockCacheManager = { + get: jest.fn(), + set: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + providers: [ + { provide: UsersService, useValue: mockUsersService }, + { provide: AuthService, useValue: mockAuthService }, + { provide: CACHE_MANAGER, useValue: mockCacheManager }, + ], + }) + .overrideGuard(AuthGuard) + .useValue({ canActivate: () => true }) + .overrideGuard(RolesGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(UsersController); + usersService = module.get(UsersService); + authService = module.get(AuthService); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); + + describe("findAll", () => { + it("should call usersService.findAll", async () => { + await controller.findAll(10, 0); + expect(usersService.findAll).toHaveBeenCalledWith(10, 0); + }); + }); + + describe("findPublicProfile", () => { + it("should call usersService.findPublicProfile", async () => { + await controller.findPublicProfile("testuser"); + expect(usersService.findPublicProfile).toHaveBeenCalledWith("testuser"); + }); + }); + + describe("findMe", () => { + it("should call usersService.findOneWithPrivateData", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.findMe(req); + expect(usersService.findOneWithPrivateData).toHaveBeenCalledWith( + "user-uuid", + ); + }); + }); + + describe("exportMe", () => { + it("should call usersService.exportUserData", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.exportMe(req); + expect(usersService.exportUserData).toHaveBeenCalledWith("user-uuid"); + }); + }); + + describe("updateMe", () => { + it("should call usersService.update", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + const dto = { displayName: "New Name" }; + await controller.updateMe(req, dto); + expect(usersService.update).toHaveBeenCalledWith("user-uuid", dto); + }); + }); + + describe("updateAvatar", () => { + it("should call usersService.updateAvatar", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + const file = {} as Express.Multer.File; + await controller.updateAvatar(req, file); + expect(usersService.updateAvatar).toHaveBeenCalledWith("user-uuid", file); + }); + }); + + describe("updateConsent", () => { + it("should call usersService.updateConsent", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + const dto = { termsVersion: "1.0", privacyVersion: "1.0" }; + await controller.updateConsent(req, dto); + expect(usersService.updateConsent).toHaveBeenCalledWith( + "user-uuid", + "1.0", + "1.0", + ); + }); + }); + + describe("removeMe", () => { + it("should call usersService.remove", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.removeMe(req); + expect(usersService.remove).toHaveBeenCalledWith("user-uuid"); + }); + }); + + describe("removeAdmin", () => { + it("should call usersService.remove", async () => { + await controller.removeAdmin("target-uuid"); + expect(usersService.remove).toHaveBeenCalledWith("target-uuid"); + }); + }); + + describe("setup2fa", () => { + it("should call authService.generateTwoFactorSecret", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.setup2fa(req); + expect(authService.generateTwoFactorSecret).toHaveBeenCalledWith( + "user-uuid", + ); + }); + }); + + describe("enable2fa", () => { + it("should call authService.enableTwoFactor", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.enable2fa(req, "token123"); + expect(authService.enableTwoFactor).toHaveBeenCalledWith( + "user-uuid", + "token123", + ); + }); + }); + + describe("disable2fa", () => { + it("should call authService.disableTwoFactor", async () => { + const req = { user: { sub: "user-uuid" } } as AuthenticatedRequest; + await controller.disable2fa(req, "token123"); + expect(authService.disableTwoFactor).toHaveBeenCalledWith( + "user-uuid", + "token123", + ); + }); + }); +});