From ade3b82e3cf2cf63ad4cb602d07763faf3e656aa Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Fri, 3 Apr 2026 19:56:58 -0700 Subject: [PATCH] fix: support partial updates in tournament PUT endpoint - Change PUT endpoint to only include fields present in the request - Add support for team configuration fields (teamDurability, partnerRotation, allowByes) - Fix contradictory test that expected resetting allowTies when not provided - When a field is not in the request, it is preserved (not modified) Fixes issue where Save Configuration button would fail with 'Tournament name is required' error. --- src/__tests__/unit/tournament-update.test.ts | 23 +++++----- src/app/api/tournaments/[id]/route.ts | 44 ++++++++++++-------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/__tests__/unit/tournament-update.test.ts b/src/__tests__/unit/tournament-update.test.ts index 2db11da..9a8bdca 100644 --- a/src/__tests__/unit/tournament-update.test.ts +++ b/src/__tests__/unit/tournament-update.test.ts @@ -101,12 +101,12 @@ describe('Tournament Update API', () => { ); }); - it('should default allowTies to false when not provided', async () => { + it('should NOT modify allowTies when not provided in request', async () => { // Mock existing tournament eventFindUniqueMock.mockImplementation(async () => ({ id: 1, name: 'Test Tournament', - allowTies: true, + allowTies: true, // This is the current value targetScore: 5, eventType: 'tournament', format: 'round_robin', @@ -119,11 +119,11 @@ describe('Tournament Update API', () => { updatedAt: new Date(), } as any)); - // Mock successful update + // Mock successful update (allowTies should remain unchanged) eventUpdateMock.mockImplementation(async () => ({ id: 1, name: 'Test Tournament', - allowTies: false, + allowTies: true, // Should remain true, not reset to false targetScore: 5, eventType: 'tournament', format: 'round_robin', @@ -141,7 +141,7 @@ describe('Tournament Update API', () => { body: JSON.stringify({ name: 'Test Tournament', targetScore: 5, - // allowTies not provided + // allowTies not provided - should NOT be modified }), }); @@ -149,13 +149,12 @@ describe('Tournament Update API', () => { const response = await PUT(request, { params }); expect(response.status).toBe(200); - expect(prisma.event.update).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - allowTies: false, // Should default to false - }), - }) - ); + // When allowTies is not provided, it should NOT be in the update data + // (it will keep its existing value in the database) + const updateCall = eventUpdateMock.mock.calls[0][0]; + expect(updateCall.data.allowTies).toBeUndefined(); + expect(updateCall.data.name).toBe('Test Tournament'); + expect(updateCall.data.targetScore).toBe(5); }); it('should preserve allowTies value when updating other fields', async () => { diff --git a/src/app/api/tournaments/[id]/route.ts b/src/app/api/tournaments/[id]/route.ts index 287d86f..72d827e 100644 --- a/src/app/api/tournaments/[id]/route.ts +++ b/src/app/api/tournaments/[id]/route.ts @@ -151,28 +151,36 @@ export async function PUT(request: Request, { params }: RouteParams) { ownerId, targetScore, allowTies, + teamDurability, + partnerRotation, + allowByes, } = body; - // Validate required fields - if (!name || typeof name !== 'string' || name.trim().length === 0) { - return NextResponse.json( - { error: "Tournament name is required" }, - { status: 400 } - ); + // Validate name only if it's being updated + if (body.hasOwnProperty('name')) { + if (!name || typeof name !== 'string' || name.trim().length === 0) { + return NextResponse.json( + { error: "Tournament name is required" }, + { status: 400 } + ); + } } - // Prepare update data - const updateData: Record = { - name: name.trim(), - description: description || null, - eventDate: eventDate ? new Date(eventDate) : null, - eventType: eventType || "tournament", - tournamentType: tournamentType || "individual", - format: format || "round_robin", - maxParticipants: maxParticipants ? parseInt(maxParticipants) : null, - targetScore: targetScore ? parseInt(targetScore) : null, - allowTies: allowTies ?? false, - }; + // Prepare update data - only include fields that are present in the request + const updateData: Record = {}; + + if (body.hasOwnProperty('name')) updateData.name = name.trim(); + if (body.hasOwnProperty('description')) updateData.description = description || null; + if (body.hasOwnProperty('eventDate')) updateData.eventDate = eventDate ? new Date(eventDate) : null; + if (body.hasOwnProperty('eventType')) updateData.eventType = eventType || "tournament"; + if (body.hasOwnProperty('tournamentType')) updateData.tournamentType = tournamentType || "individual"; + if (body.hasOwnProperty('format')) updateData.format = format || "round_robin"; + if (body.hasOwnProperty('maxParticipants')) updateData.maxParticipants = maxParticipants ? parseInt(maxParticipants) : null; + if (body.hasOwnProperty('targetScore')) updateData.targetScore = targetScore ? parseInt(targetScore) : null; + if (body.hasOwnProperty('allowTies')) updateData.allowTies = allowTies ?? false; + if (body.hasOwnProperty('teamDurability')) updateData.teamDurability = teamDurability || "permanent"; + if (body.hasOwnProperty('partnerRotation')) updateData.partnerRotation = partnerRotation || "none"; + if (body.hasOwnProperty('allowByes')) updateData.allowByes = allowByes ?? true; // Only allow status updates if they don't conflict with auto-calculation if (status) {