Add interface design references and principles; implement sport service tests
- Introduced critique, example, principles, and validation documents for interface design. - Enhanced backend service with coverage threshold and fixed variable declaration in sport service. - Added comprehensive tests for create, update, and list sports functionalities.
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
[test]
|
||||
preload = ["./test/support/prisma.mock.ts"]
|
||||
coverageThreshold = 0.8
|
||||
@@ -71,7 +71,7 @@ export async function createSport(input: CreateSportInput) {
|
||||
return err(Errors.conflict('Ya existe un deporte con ese nombre.'));
|
||||
}
|
||||
|
||||
var created = db.sport.create({
|
||||
const created = await db.sport.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
name: input.name.trim(),
|
||||
|
||||
97
apps/backend/test/sport/create-sport.handler.test.ts
Normal file
97
apps/backend/test/sport/create-sport.handler.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
||||
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { err, ok } from '@/lib/result';
|
||||
import type { CreateSportInput } from '@repo/api-contract';
|
||||
|
||||
const createSportMock = mock(async (_input: CreateSportInput) =>
|
||||
ok({
|
||||
id: 'sport-1',
|
||||
name: 'Tenis',
|
||||
slug: 'tenis',
|
||||
isActive: true,
|
||||
createdAt: '2026-04-23T00:00:00.000Z',
|
||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
||||
})
|
||||
);
|
||||
|
||||
mock.module('@/modules/sport/services/sport.service', () => ({
|
||||
__esModule: true,
|
||||
createSport: createSportMock,
|
||||
}));
|
||||
|
||||
const { createSportHandler } = await import(
|
||||
'@/modules/sport/handlers/create-sport.handler'
|
||||
);
|
||||
|
||||
type HandlerContext = {
|
||||
get: (key: 'requestId') => string | undefined;
|
||||
req: {
|
||||
valid: () => unknown;
|
||||
};
|
||||
json: ReturnType<typeof mock>;
|
||||
};
|
||||
|
||||
function createContext(body: unknown) {
|
||||
const json = mock((payload: unknown, init?: unknown) => ({
|
||||
payload,
|
||||
init,
|
||||
}));
|
||||
|
||||
const c = {
|
||||
get: () => undefined,
|
||||
req: {
|
||||
valid: () => body,
|
||||
},
|
||||
json,
|
||||
} as unknown as HandlerContext;
|
||||
|
||||
return { c, json };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
createSportMock.mockReset();
|
||||
mock.clearAllMocks();
|
||||
});
|
||||
|
||||
test('returns 201 with the created sport', async () => {
|
||||
const createdSport = {
|
||||
id: 'sport-1',
|
||||
name: 'Tenis',
|
||||
slug: 'tenis',
|
||||
isActive: true,
|
||||
createdAt: '2026-04-23T00:00:00.000Z',
|
||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
||||
} as const;
|
||||
|
||||
createSportMock.mockResolvedValue(ok(createdSport));
|
||||
|
||||
const payload: CreateSportInput = { name: ' Tenis ' };
|
||||
const { c, json } = createContext(payload);
|
||||
|
||||
const response = await createSportHandler(c as never);
|
||||
|
||||
expect(createSportMock).toHaveBeenCalledWith(payload);
|
||||
expect(json.mock.calls[0]?.[0]).toEqual(createdSport);
|
||||
expect(json.mock.calls[0]?.[1]).toBe(201);
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
test('returns a conflict problem when the service rejects with a conflict error', async () => {
|
||||
createSportMock.mockResolvedValue(err(Errors.conflict('Ya existe un deporte con ese nombre.')));
|
||||
|
||||
const payload: CreateSportInput = { name: 'Tenis' };
|
||||
const { c, json } = createContext(payload);
|
||||
|
||||
const response = await createSportHandler(c as never);
|
||||
|
||||
expect(createSportMock).toHaveBeenCalledWith(payload);
|
||||
expect(json.mock.calls[0]?.[0]).toEqual({
|
||||
type: 'https://api.myapp.dev/problems/conflict',
|
||||
title: 'Conflict',
|
||||
status: 409,
|
||||
detail: 'Ya existe un deporte con ese nombre.',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[1]).toBe(409);
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
199
apps/backend/test/sport/sport.service.test.ts
Normal file
199
apps/backend/test/sport/sport.service.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
||||
|
||||
import { createPrismaMock } from 'bun-mock-prisma';
|
||||
|
||||
import type { PrismaClient } from '@/generated/prisma/client';
|
||||
import type { PrismaClientMock } from 'bun-mock-prisma';
|
||||
|
||||
const prismaMock = createPrismaMock<PrismaClient>() as PrismaClientMock<PrismaClient>;
|
||||
const uuidV7Mock = mock(() => '00000000-0000-4000-8000-000000000001');
|
||||
|
||||
mock.module('@/lib/prisma', () => ({
|
||||
__esModule: true,
|
||||
db: {
|
||||
sport: prismaMock.sport,
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('uuid', () => ({
|
||||
__esModule: true,
|
||||
v7: uuidV7Mock,
|
||||
}));
|
||||
|
||||
const { createSport, getSportById, listSports, updateSport } = await import(
|
||||
'@/modules/sport/services/sport.service'
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
prismaMock._reset();
|
||||
mock.clearAllMocks();
|
||||
});
|
||||
|
||||
test('listSports returns sports ordered by name', async () => {
|
||||
const sports = [
|
||||
{
|
||||
id: 'sport-2',
|
||||
name: 'Tenis',
|
||||
slug: 'tenis',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-22T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-22T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: 'sport-1',
|
||||
name: 'Básquet',
|
||||
slug: 'basquet',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-21T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-21T00:00:00.000Z'),
|
||||
},
|
||||
];
|
||||
|
||||
prismaMock.sport.findMany.mockResolvedValue(sports as never);
|
||||
|
||||
const result = await listSports();
|
||||
|
||||
expect(prismaMock.sport.findMany).toHaveBeenCalledWith({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
value: sports,
|
||||
});
|
||||
});
|
||||
|
||||
test('getSportById returns the requested sport', async () => {
|
||||
const sport = {
|
||||
id: 'sport-1',
|
||||
name: 'Tenis',
|
||||
slug: 'tenis',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-22T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-22T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
prismaMock.sport.findUnique.mockResolvedValue(sport as never);
|
||||
|
||||
const result = await getSportById('sport-1');
|
||||
|
||||
expect(prismaMock.sport.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'sport-1' },
|
||||
});
|
||||
expect(result).toEqual(sport);
|
||||
});
|
||||
|
||||
test('createSport trims the name and creates a unique slug', async () => {
|
||||
const createdSport = {
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
name: 'Fútbol 7!!',
|
||||
slug: 'futbol-7',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
prismaMock.sport.findFirst.mockResolvedValue(null as never);
|
||||
prismaMock.sport.count.mockResolvedValue(0 as never);
|
||||
prismaMock.sport.create.mockResolvedValue(createdSport as never);
|
||||
|
||||
const result = await createSport({ name: ' Fútbol 7!! ' });
|
||||
|
||||
expect(uuidV7Mock).toHaveBeenCalledTimes(1);
|
||||
expect(prismaMock.sport.findFirst).toHaveBeenCalledWith({
|
||||
where: { slug: 'futbol-7' },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(prismaMock.sport.count).toHaveBeenCalledWith({
|
||||
where: { slug: 'futbol-7' },
|
||||
});
|
||||
expect(prismaMock.sport.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
name: 'Fútbol 7!!',
|
||||
slug: 'futbol-7',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
value: createdSport,
|
||||
});
|
||||
});
|
||||
|
||||
test('createSport retries with a numeric suffix when the slug already exists', async () => {
|
||||
const createdSport = {
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
name: 'Tenis',
|
||||
slug: 'tenis-2',
|
||||
isActive: true,
|
||||
createdAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
prismaMock.sport.findFirst
|
||||
.mockResolvedValueOnce({ id: 'existing-sport' } as never)
|
||||
.mockResolvedValueOnce(null as never);
|
||||
prismaMock.sport.count.mockResolvedValue(0 as never);
|
||||
prismaMock.sport.create.mockResolvedValue(createdSport as never);
|
||||
|
||||
const result = await createSport({ name: 'Tenis' });
|
||||
|
||||
expect(prismaMock.sport.findFirst).toHaveBeenNthCalledWith(1, {
|
||||
where: { slug: 'tenis' },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(prismaMock.sport.findFirst).toHaveBeenNthCalledWith(2, {
|
||||
where: { slug: 'tenis-2' },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(prismaMock.sport.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
name: 'Tenis',
|
||||
slug: 'tenis-2',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
value: createdSport,
|
||||
});
|
||||
});
|
||||
|
||||
test('updateSport updates the name, slug and active flag', async () => {
|
||||
const updatedSport = {
|
||||
id: 'sport-1',
|
||||
name: 'Básquet 3x3',
|
||||
slug: 'basquet-3x3',
|
||||
isActive: false,
|
||||
createdAt: new Date('2026-04-21T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-04-23T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
prismaMock.sport.findFirst.mockResolvedValue(null as never);
|
||||
prismaMock.sport.update.mockResolvedValue(updatedSport as never);
|
||||
|
||||
const result = await updateSport('sport-1', {
|
||||
name: 'Básquet 3x3',
|
||||
isActive: false,
|
||||
});
|
||||
|
||||
expect(prismaMock.sport.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
slug: 'basquet-3x3',
|
||||
id: {
|
||||
not: 'sport-1',
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
expect(prismaMock.sport.update).toHaveBeenCalledWith({
|
||||
where: { id: 'sport-1' },
|
||||
data: {
|
||||
name: 'Básquet 3x3',
|
||||
slug: 'basquet-3x3',
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(updatedSport);
|
||||
});
|
||||
Reference in New Issue
Block a user