Refactor to vertical slice
This commit is contained in:
@@ -1,27 +1,22 @@
|
||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
||||
|
||||
const rescheduleAdminBookingMock = mock(
|
||||
async (_userId: string, _id: string, _input: unknown) => ({})
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { err, ok } from '@/lib/result';
|
||||
|
||||
const rescheduleAdminBookingMock = mock(async (_userId: string, _id: string, _input: unknown) =>
|
||||
ok({})
|
||||
);
|
||||
|
||||
class MockAdminBookingServiceError extends Error {
|
||||
status: 400 | 403 | 404 | 409;
|
||||
|
||||
constructor(message: string, status: 400 | 403 | 404 | 409 = 400) {
|
||||
super(message);
|
||||
this.name = 'AdminBookingServiceError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
mock.module('@/modules/admin-booking/services/admin-booking.service', () => ({
|
||||
__esModule: true,
|
||||
AdminBookingServiceError: MockAdminBookingServiceError,
|
||||
rescheduleAdminBooking: rescheduleAdminBookingMock,
|
||||
}));
|
||||
mock.module(
|
||||
'@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.business',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
rescheduleAdminBooking: rescheduleAdminBookingMock,
|
||||
})
|
||||
);
|
||||
|
||||
const { rescheduleAdminBookingHandler } = await import(
|
||||
'@/modules/admin-booking/handlers/reschedule-admin-booking.handler'
|
||||
'@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.handler'
|
||||
);
|
||||
|
||||
type HandlerContext = {
|
||||
@@ -82,7 +77,7 @@ test('returns 200 with the updated booking', async () => {
|
||||
updatedAt: '2026-07-14T00:00:00.000Z',
|
||||
} as const;
|
||||
|
||||
rescheduleAdminBookingMock.mockResolvedValue(updatedBooking);
|
||||
rescheduleAdminBookingMock.mockResolvedValue(ok(updatedBooking));
|
||||
|
||||
const { c, json } = createContext({
|
||||
userId: 'user-1',
|
||||
@@ -96,16 +91,13 @@ test('returns 200 with the updated booking', async () => {
|
||||
startTime: '14:00',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[0]).toEqual(updatedBooking);
|
||||
expect(json.mock.calls[0]?.[1]).toBeUndefined();
|
||||
expect(json.mock.calls[0]?.[1]).toBe(200);
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
test('maps AdminBookingServiceError to an error response', async () => {
|
||||
rescheduleAdminBookingMock.mockRejectedValue(
|
||||
new MockAdminBookingServiceError(
|
||||
'Solo se pueden reprogramar reservas en estado confirmada.',
|
||||
409
|
||||
)
|
||||
rescheduleAdminBookingMock.mockResolvedValue(
|
||||
err(Errors.conflict('Solo se pueden reprogramar reservas en estado confirmada.'))
|
||||
);
|
||||
|
||||
const { c, json } = createContext({
|
||||
@@ -119,7 +111,10 @@ test('maps AdminBookingServiceError to an error response', async () => {
|
||||
startTime: '14:00',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[0]).toEqual({
|
||||
message: 'Solo se pueden reprogramar reservas en estado confirmada.',
|
||||
type: 'https://api.myapp.dev/problems/conflict',
|
||||
title: 'Conflict',
|
||||
status: 409,
|
||||
detail: 'Solo se pueden reprogramar reservas en estado confirmada.',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[1]).toBe(409);
|
||||
expect(response).toBeDefined();
|
||||
|
||||
@@ -2,8 +2,8 @@ import { beforeEach, expect, test } from 'bun:test';
|
||||
|
||||
import { prismaMock, transactionMock, uuidV7Mock } from '../support/prisma.mock';
|
||||
|
||||
const { AdminBookingServiceError, rescheduleAdminBooking } = await import(
|
||||
'@/modules/admin-booking/services/admin-booking.service'
|
||||
const { rescheduleAdminBooking } = await import(
|
||||
'@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.business'
|
||||
);
|
||||
|
||||
function makeBooking(overrides?: Record<string, unknown>) {
|
||||
@@ -84,9 +84,11 @@ test('reschedules a booking changing both court and time', async () => {
|
||||
startTime: '14:00',
|
||||
});
|
||||
|
||||
expect(result.courtId).toBe('court-2');
|
||||
expect(result.startTime).toBe('14:00');
|
||||
expect(result.courtName).toBe('Cancha 2');
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.courtId).toBe('court-2');
|
||||
expect(result.value.startTime).toBe('14:00');
|
||||
expect(result.value.courtName).toBe('Cancha 2');
|
||||
expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1);
|
||||
expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({
|
||||
where: { id: 'booking-1' },
|
||||
@@ -137,8 +139,10 @@ test('reschedules a booking changing only the time', async () => {
|
||||
startTime: '15:00',
|
||||
});
|
||||
|
||||
expect(result.startTime).toBe('15:00');
|
||||
expect(result.courtId).toBe('court-1');
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.startTime).toBe('15:00');
|
||||
expect(result.value.courtId).toBe('court-1');
|
||||
expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1);
|
||||
expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({
|
||||
where: { id: 'booking-1' },
|
||||
@@ -153,15 +157,11 @@ test('reschedules a booking changing only the time', async () => {
|
||||
test('rejects when booking is not found', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(null as never);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'non-existent', { startTime: '14:00' });
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'non-existent', { startTime: '14:00' });
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(404);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('not_found');
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -170,15 +170,11 @@ test('rejects when booking status is not CONFIRMED', async () => {
|
||||
makeBooking({ status: 'COMPLETED' }) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '14:00' });
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '14:00' });
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('conflict');
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -194,18 +190,14 @@ test('rejects when slot is not available in target court', async () => {
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('conflict');
|
||||
});
|
||||
|
||||
test('rejects when target court has a different sport', async () => {
|
||||
@@ -216,18 +208,14 @@ test('rejects when target court has a different sport', async () => {
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('conflict');
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -239,18 +227,14 @@ test('rejects when target court is under maintenance', async () => {
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('conflict');
|
||||
});
|
||||
|
||||
test('rejects when there is an overlapping booking', async () => {
|
||||
@@ -266,33 +250,25 @@ test('rejects when there is an overlapping booking', async () => {
|
||||
}) as never
|
||||
);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'court-2',
|
||||
startTime: '14:00',
|
||||
});
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(409);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('conflict');
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects when neither courtId nor startTime changes', async () => {
|
||||
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {});
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(400);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('validation');
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -301,17 +277,13 @@ test('rejects when target court does not belong to the same complex', async () =
|
||||
// Court belongs to a different complex
|
||||
prismaMock.court.findFirst.mockResolvedValue(null as never);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'other-complex-court',
|
||||
startTime: '14:00',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
|
||||
courtId: 'other-complex-court',
|
||||
startTime: '14:00',
|
||||
});
|
||||
|
||||
expect(caught).toBeInstanceOf(AdminBookingServiceError);
|
||||
expect((caught as InstanceType<typeof AdminBookingServiceError>).status).toBe(404);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.type).toBe('not_found');
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { beforeEach, expect, mock, test } from 'bun:test';
|
||||
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { err, ok } from '@/lib/result';
|
||||
import type { InviteComplexUserResponse } from '@repo/api-contract';
|
||||
|
||||
const inviteComplexUserMock = mock(async () => undefined as unknown as InviteComplexUserResponse);
|
||||
|
||||
class MockComplexMembersError extends Error {
|
||||
status: 400 | 403 | 404 | 409;
|
||||
|
||||
constructor(message: string, status: 400 | 403 | 404 | 409 = 400) {
|
||||
super(message);
|
||||
this.name = 'ComplexMembersError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
const { createInviteComplexUserHandler } = await import(
|
||||
'@/modules/complex/handlers/invite-complex-user.handler'
|
||||
'@/modules/complex/features/invite-complex-user/invite-complex-user.handler'
|
||||
);
|
||||
|
||||
const inviteComplexUserHandler = createInviteComplexUserHandler({
|
||||
inviteComplexUser: inviteComplexUserMock,
|
||||
ComplexMembersError: MockComplexMembersError,
|
||||
});
|
||||
|
||||
type HandlerContext = {
|
||||
@@ -78,7 +69,7 @@ test('returns 201 with the service result', async () => {
|
||||
},
|
||||
} as const;
|
||||
|
||||
inviteComplexUserMock.mockResolvedValue(serviceResult);
|
||||
inviteComplexUserMock.mockResolvedValue(ok(serviceResult));
|
||||
|
||||
const { c, json } = createContext({
|
||||
userId: 'admin-1',
|
||||
@@ -99,8 +90,8 @@ test('returns 201 with the service result', async () => {
|
||||
});
|
||||
|
||||
test('maps ComplexMembersError to an error response', async () => {
|
||||
inviteComplexUserMock.mockRejectedValue(
|
||||
new MockComplexMembersError('Solo un ADMIN puede administrar usuarios.', 403)
|
||||
inviteComplexUserMock.mockResolvedValue(
|
||||
err(Errors.forbidden('Solo un ADMIN puede administrar usuarios.'))
|
||||
);
|
||||
|
||||
const { c, json } = createContext({
|
||||
@@ -117,8 +108,11 @@ test('maps ComplexMembersError to an error response', async () => {
|
||||
{ email: 'new.member@example.com' }
|
||||
);
|
||||
expect(json.mock.calls[0]?.[0]).toEqual({
|
||||
message: 'Solo un ADMIN puede administrar usuarios.',
|
||||
type: 'https://api.myapp.dev/problems/forbidden',
|
||||
title: 'Forbidden',
|
||||
status: 403,
|
||||
detail: 'Solo un ADMIN puede administrar usuarios.',
|
||||
});
|
||||
expect(json.mock.calls[0]?.[1]).toEqual({ status: 403 });
|
||||
expect(json.mock.calls[0]?.[1]).toBe(403);
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import { beforeEach, expect, test } from 'bun:test';
|
||||
|
||||
import { prismaMock, sendMailMock, transactionMock } from '../support/prisma.mock';
|
||||
|
||||
const { ComplexMembersError, inviteComplexUser } = await import(
|
||||
'@/modules/complex/services/complex-members.service'
|
||||
const { inviteComplexUser } = await import(
|
||||
'@/modules/complex/features/invite-complex-user/invite-complex-user.business'
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -32,8 +32,10 @@ test('creates a pending invitation and sends the email', async () => {
|
||||
email: ' New.Member@example.com ',
|
||||
});
|
||||
|
||||
expect(result.invitation.email).toBe('new.member@example.com');
|
||||
expect(result.invitation.status).toBe('PENDING');
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.invitation.email).toBe('new.member@example.com');
|
||||
expect(result.value.invitation.status).toBe('PENDING');
|
||||
expect(prismaMock.complexInvitation.create).toHaveBeenCalledTimes(1);
|
||||
|
||||
const createArgs = prismaMock.complexInvitation.create.mock.calls[0]?.[0] as {
|
||||
@@ -69,18 +71,11 @@ test('rejects when the invite target already belongs to the complex', async () =
|
||||
prismaMock.complex.findUnique.mockResolvedValue({ complexName: 'Playzer Norte' } as never);
|
||||
prismaMock.user.findUnique.mockResolvedValue({ id: 'member-1' } as never);
|
||||
|
||||
let caught: unknown;
|
||||
const result = await inviteComplexUser('admin-1', 'complex-1', {
|
||||
email: 'member@example.com',
|
||||
});
|
||||
|
||||
try {
|
||||
await inviteComplexUser('admin-1', 'complex-1', {
|
||||
email: 'member@example.com',
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(ComplexMembersError);
|
||||
expect((caught as InstanceType<typeof ComplexMembersError>).status).toBe(409);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
expect(sendMailMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -15,12 +15,11 @@ const createSportMock = mock(async (_input: CreateSportInput) =>
|
||||
})
|
||||
);
|
||||
|
||||
mock.module('@/modules/sport/services/sport.service', () => ({
|
||||
__esModule: true,
|
||||
createSport: createSportMock,
|
||||
}));
|
||||
const { createCreateSportHandler } = await import(
|
||||
'@/modules/sport/features/create-sport/create-sport.handler'
|
||||
);
|
||||
|
||||
const { createSportHandler } = await import('@/modules/sport/handlers/create-sport.handler');
|
||||
const createSportHandler = createCreateSportHandler({ createSport: createSportMock });
|
||||
|
||||
type HandlerContext = {
|
||||
get: (key: 'requestId') => string | undefined;
|
||||
|
||||
@@ -2,9 +2,11 @@ import { beforeEach, expect, test } from 'bun:test';
|
||||
|
||||
import { prismaMock, uuidV7Mock } from '../support/prisma.mock';
|
||||
|
||||
const { createSport, getSportById, listSports, updateSport } = await import(
|
||||
'@/modules/sport/services/sport.service'
|
||||
const { createSport } = await import('@/modules/sport/features/create-sport/create-sport.business');
|
||||
const { getSportById, updateSport } = await import(
|
||||
'@/modules/sport/features/update-sport/update-sport.business'
|
||||
);
|
||||
const { listSports } = await import('@/modules/sport/features/list-sports/list-sports.business');
|
||||
|
||||
beforeEach(() => {
|
||||
prismaMock._reset();
|
||||
|
||||
Reference in New Issue
Block a user