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:
@@ -1,58 +1,43 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { DatabaseService } from "../database/database.service";
|
||||
import { apiKeys } from "../database/schemas";
|
||||
import { HashingService } from "../crypto/services/hashing.service";
|
||||
import { ApiKeysService } from "./api-keys.service";
|
||||
import { ApiKeysRepository } from "./repositories/api-keys.repository";
|
||||
|
||||
describe("ApiKeysService", () => {
|
||||
let service: ApiKeysService;
|
||||
let repository: ApiKeysRepository;
|
||||
|
||||
const mockDb = {
|
||||
insert: jest.fn(),
|
||||
values: jest.fn(),
|
||||
select: jest.fn(),
|
||||
from: jest.fn(),
|
||||
where: jest.fn(),
|
||||
limit: jest.fn(),
|
||||
update: jest.fn(),
|
||||
set: jest.fn(),
|
||||
returning: jest.fn(),
|
||||
const mockApiKeysRepository = {
|
||||
create: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
revoke: jest.fn(),
|
||||
findActiveByKeyHash: jest.fn(),
|
||||
updateLastUsed: jest.fn(),
|
||||
};
|
||||
|
||||
const mockHashingService = {
|
||||
hashSha256: jest.fn().mockResolvedValue("hashed-key"),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockDb.insert.mockReturnThis();
|
||||
mockDb.values.mockResolvedValue(undefined);
|
||||
mockDb.select.mockReturnThis();
|
||||
mockDb.from.mockReturnThis();
|
||||
mockDb.where.mockReturnThis();
|
||||
mockDb.limit.mockReturnThis();
|
||||
mockDb.update.mockReturnThis();
|
||||
mockDb.set.mockReturnThis();
|
||||
mockDb.returning.mockResolvedValue([]);
|
||||
|
||||
// Default for findAll which is awaited on where()
|
||||
mockDb.where.mockImplementation(() => {
|
||||
const chain = {
|
||||
returning: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
return Object.assign(Promise.resolve([]), chain);
|
||||
});
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ApiKeysService,
|
||||
{
|
||||
provide: DatabaseService,
|
||||
useValue: {
|
||||
db: mockDb,
|
||||
},
|
||||
provide: ApiKeysRepository,
|
||||
useValue: mockApiKeysRepository,
|
||||
},
|
||||
{
|
||||
provide: HashingService,
|
||||
useValue: mockHashingService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ApiKeysService>(ApiKeysService);
|
||||
repository = module.get<ApiKeysRepository>(ApiKeysRepository);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
@@ -67,8 +52,7 @@ describe("ApiKeysService", () => {
|
||||
|
||||
const result = await service.create(userId, name, expiresAt);
|
||||
|
||||
expect(mockDb.insert).toHaveBeenCalledWith(apiKeys);
|
||||
expect(mockDb.values).toHaveBeenCalledWith(
|
||||
expect(repository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId,
|
||||
name,
|
||||
@@ -87,12 +71,11 @@ describe("ApiKeysService", () => {
|
||||
it("should find all API keys for a user", async () => {
|
||||
const userId = "user-id";
|
||||
const expectedKeys = [{ id: "1", name: "Key 1" }];
|
||||
(mockDb.where as jest.Mock).mockResolvedValue(expectedKeys);
|
||||
mockApiKeysRepository.findAll.mockResolvedValue(expectedKeys);
|
||||
|
||||
const result = await service.findAll(userId);
|
||||
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalledWith(apiKeys);
|
||||
expect(repository.findAll).toHaveBeenCalledWith(userId);
|
||||
expect(result).toEqual(expectedKeys);
|
||||
});
|
||||
});
|
||||
@@ -102,17 +85,11 @@ describe("ApiKeysService", () => {
|
||||
const userId = "user-id";
|
||||
const keyId = "key-id";
|
||||
const expectedResult = [{ id: keyId, isActive: false }];
|
||||
|
||||
mockDb.where.mockReturnValue({
|
||||
returning: jest.fn().mockResolvedValue(expectedResult),
|
||||
});
|
||||
mockApiKeysRepository.revoke.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await service.revoke(userId, keyId);
|
||||
|
||||
expect(mockDb.update).toHaveBeenCalledWith(apiKeys);
|
||||
expect(mockDb.set).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ isActive: false }),
|
||||
);
|
||||
expect(repository.revoke).toHaveBeenCalledWith(userId, keyId);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
@@ -120,42 +97,19 @@ describe("ApiKeysService", () => {
|
||||
describe("validateKey", () => {
|
||||
it("should validate a valid API key", async () => {
|
||||
const key = "mg_live_testkey";
|
||||
const keyHash = createHash("sha256").update(key).digest("hex");
|
||||
const apiKey = { id: "1", keyHash, isActive: true, expiresAt: null };
|
||||
|
||||
(mockDb.limit as jest.Mock).mockResolvedValue([apiKey]);
|
||||
(mockDb.where as jest.Mock).mockResolvedValue([apiKey]); // For the update later if needed, but here it's for select
|
||||
|
||||
// We need to be careful with chaining mockDb.where is used in both select and update
|
||||
const mockSelect = {
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockResolvedValue([apiKey]),
|
||||
};
|
||||
const mockUpdate = {
|
||||
set: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
(mockDb.select as jest.Mock).mockReturnValue(mockSelect);
|
||||
(mockDb.update as jest.Mock).mockReturnValue(mockUpdate);
|
||||
const apiKey = { id: "1", isActive: true, expiresAt: null };
|
||||
mockApiKeysRepository.findActiveByKeyHash.mockResolvedValue(apiKey);
|
||||
|
||||
const result = await service.validateKey(key);
|
||||
|
||||
expect(result).toEqual(apiKey);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.update).toHaveBeenCalledWith(apiKeys);
|
||||
expect(repository.findActiveByKeyHash).toHaveBeenCalled();
|
||||
expect(repository.updateLastUsed).toHaveBeenCalledWith(apiKey.id);
|
||||
});
|
||||
|
||||
it("should return null for invalid API key", async () => {
|
||||
(mockDb.select as jest.Mock).mockReturnValue({
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
|
||||
mockApiKeysRepository.findActiveByKeyHash.mockResolvedValue(null);
|
||||
const result = await service.validateKey("invalid-key");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@@ -164,12 +118,7 @@ describe("ApiKeysService", () => {
|
||||
const expiredDate = new Date();
|
||||
expiredDate.setFullYear(expiredDate.getFullYear() - 1);
|
||||
const apiKey = { id: "1", isActive: true, expiresAt: expiredDate };
|
||||
|
||||
(mockDb.select as jest.Mock).mockReturnValue({
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockResolvedValue([apiKey]),
|
||||
});
|
||||
mockApiKeysRepository.findActiveByKeyHash.mockResolvedValue(apiKey);
|
||||
|
||||
const result = await service.validateKey(key);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user