feat: add modular services and repositories for improved code organization
Introduce repository pattern across multiple services, including `favorites`, `tags`, `sessions`, `reports`, `auth`, and more. Decouple crypto functionalities into modular services like `HashingService`, `JwtService`, and `EncryptionService`. Improve testability and maintainability by simplifying dependencies and consolidating utility logic.
This commit is contained in:
@@ -13,60 +13,45 @@ jest.mock("jose", () => ({
|
||||
|
||||
import { UnauthorizedException } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { CryptoService } from "../crypto/crypto.service";
|
||||
import { DatabaseService } from "../database/database.service";
|
||||
import { HashingService } from "../crypto/services/hashing.service";
|
||||
import { JwtService } from "../crypto/services/jwt.service";
|
||||
import { SessionsService } from "./sessions.service";
|
||||
import { SessionsRepository } from "./repositories/sessions.repository";
|
||||
|
||||
describe("SessionsService", () => {
|
||||
let service: SessionsService;
|
||||
let repository: SessionsRepository;
|
||||
|
||||
const mockDb = {
|
||||
insert: jest.fn(),
|
||||
select: jest.fn(),
|
||||
const mockSessionsRepository = {
|
||||
create: jest.fn(),
|
||||
findValidByRefreshToken: jest.fn(),
|
||||
update: jest.fn(),
|
||||
revoke: jest.fn(),
|
||||
revokeAllByUserId: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCryptoService = {
|
||||
generateJwt: jest.fn().mockResolvedValue("mock-jwt"),
|
||||
hashIp: jest.fn().mockResolvedValue("mock-ip-hash"),
|
||||
const mockHashingService = {
|
||||
hashIp: jest.fn().mockResolvedValue("hashed-ip"),
|
||||
};
|
||||
|
||||
const mockJwtService = {
|
||||
generateJwt: jest.fn().mockResolvedValue("new-token"),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const chain = {
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn().mockReturnThis(),
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
// biome-ignore lint/suspicious/noThenProperty: Fine for testing purposes
|
||||
then: jest.fn().mockImplementation(function (cb) {
|
||||
return Promise.resolve(this).then(cb);
|
||||
}),
|
||||
};
|
||||
|
||||
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
|
||||
for (const mock of Object.values(chain)) {
|
||||
if (mock !== chain.then) {
|
||||
mock.mockImplementation(mockImplementation);
|
||||
}
|
||||
}
|
||||
Object.assign(mockDb, chain);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SessionsService,
|
||||
{ provide: DatabaseService, useValue: { db: mockDb } },
|
||||
{ provide: CryptoService, useValue: mockCryptoService },
|
||||
{ provide: SessionsRepository, useValue: mockSessionsRepository },
|
||||
{ provide: HashingService, useValue: mockHashingService },
|
||||
{ provide: JwtService, useValue: mockJwtService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SessionsService>(SessionsService);
|
||||
repository = module.get<SessionsRepository>(SessionsRepository);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
@@ -75,13 +60,10 @@ describe("SessionsService", () => {
|
||||
|
||||
describe("createSession", () => {
|
||||
it("should create a session", async () => {
|
||||
mockDb.returning.mockResolvedValue([{ id: "s1", refreshToken: "mock-jwt" }]);
|
||||
mockSessionsRepository.create.mockResolvedValue({ id: "s1" });
|
||||
const result = await service.createSession("u1", "agent", "1.2.3.4");
|
||||
expect(result.id).toBe("s1");
|
||||
expect(mockCryptoService.generateJwt).toHaveBeenCalledWith(
|
||||
{ sub: "u1", type: "refresh" },
|
||||
"7d",
|
||||
);
|
||||
expect(result).toEqual({ id: "s1" });
|
||||
expect(repository.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,36 +71,46 @@ describe("SessionsService", () => {
|
||||
it("should refresh a valid session", async () => {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + 1);
|
||||
const session = { id: "s1", userId: "u1", expiresAt, isValid: true };
|
||||
|
||||
mockDb.where.mockImplementation(() => {
|
||||
const chain = {
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
returning: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ ...session, refreshToken: "new-jwt" }]),
|
||||
// biome-ignore lint/suspicious/noThenProperty: Fine for testing purposes
|
||||
then: jest.fn().mockResolvedValue(session),
|
||||
};
|
||||
return Object.assign(Promise.resolve([session]), chain);
|
||||
mockSessionsRepository.findValidByRefreshToken.mockResolvedValue({
|
||||
id: "s1",
|
||||
userId: "u1",
|
||||
expiresAt,
|
||||
});
|
||||
mockSessionsRepository.update.mockResolvedValue({ id: "s1", refreshToken: "new-token" });
|
||||
|
||||
const result = await service.refreshSession("old-jwt");
|
||||
expect(result.refreshToken).toBe("new-jwt");
|
||||
const result = await service.refreshSession("old-token");
|
||||
|
||||
expect(result.refreshToken).toBe("new-token");
|
||||
expect(repository.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw UnauthorizedException if session not found", async () => {
|
||||
mockDb.where.mockImplementation(() => {
|
||||
const chain = {
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
// biome-ignore lint/suspicious/noThenProperty: Fine for testing purposes
|
||||
then: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
return Object.assign(Promise.resolve([]), chain);
|
||||
});
|
||||
it("should throw if session not found", async () => {
|
||||
mockSessionsRepository.findValidByRefreshToken.mockResolvedValue(null);
|
||||
await expect(service.refreshSession("invalid")).rejects.toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw and revoke if session expired", async () => {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() - 1);
|
||||
mockSessionsRepository.findValidByRefreshToken.mockResolvedValue({
|
||||
id: "s1",
|
||||
userId: "u1",
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
await expect(service.refreshSession("expired")).rejects.toThrow(
|
||||
UnauthorizedException,
|
||||
);
|
||||
expect(repository.revoke).toHaveBeenCalledWith("s1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("revokeSession", () => {
|
||||
it("should revoke a session", async () => {
|
||||
await service.revokeSession("s1");
|
||||
expect(repository.revoke).toHaveBeenCalledWith("s1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user