From 94c7c7de35abb8812cad87aa3a36cdf767b386f4 Mon Sep 17 00:00:00 2001 From: TyostoKarry <114697841+TyostoKarry@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:46:33 +0300 Subject: [PATCH 1/3] Add endpoint to fetch suitable event dates for all participants - Implemented GET /api/v1/event/{id}/results to return dates suitable for all participants. - Added EventResultResponse DTO with companion logic to compute suitable dates. Chose to invoke getEventById() directly in the controller instead of adding a separate service method, as the result computation is a simple derived transformation rather than service-layer logic. When no votes exist or no suitable dates are found, an empty list is returned instead of raising an error. --- .../controller/EventController.kt | 10 +++++ .../eventshuffle/dto/EventResultResponse.kt | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/kotlin/io/github/tyostokarry/eventshuffle/dto/EventResultResponse.kt diff --git a/src/main/kotlin/io/github/tyostokarry/eventshuffle/controller/EventController.kt b/src/main/kotlin/io/github/tyostokarry/eventshuffle/controller/EventController.kt index 76b3dea..6bfa8ff 100644 --- a/src/main/kotlin/io/github/tyostokarry/eventshuffle/controller/EventController.kt +++ b/src/main/kotlin/io/github/tyostokarry/eventshuffle/controller/EventController.kt @@ -5,6 +5,7 @@ import io.github.tyostokarry.eventshuffle.dto.EventCreateResponse import io.github.tyostokarry.eventshuffle.dto.EventDetailsResponse import io.github.tyostokarry.eventshuffle.dto.EventListItemDto import io.github.tyostokarry.eventshuffle.dto.EventListResponse +import io.github.tyostokarry.eventshuffle.dto.EventResultResponse import io.github.tyostokarry.eventshuffle.dto.VoteCreateRequest import io.github.tyostokarry.eventshuffle.entity.Event import io.github.tyostokarry.eventshuffle.service.EventService @@ -57,4 +58,13 @@ class EventController( val response = EventDetailsResponse.fromEvent(event) return ResponseEntity.ok(response) } + + @GetMapping("/{id}/results") + fun getEventResults( + @PathVariable id: Long, + ): ResponseEntity { + val event = eventService.getEventById(id) + val response = EventResultResponse.fromEvent(event) + return ResponseEntity.ok(response) + } } diff --git a/src/main/kotlin/io/github/tyostokarry/eventshuffle/dto/EventResultResponse.kt b/src/main/kotlin/io/github/tyostokarry/eventshuffle/dto/EventResultResponse.kt new file mode 100644 index 0000000..b90835a --- /dev/null +++ b/src/main/kotlin/io/github/tyostokarry/eventshuffle/dto/EventResultResponse.kt @@ -0,0 +1,42 @@ +package io.github.tyostokarry.eventshuffle.dto + +import io.github.tyostokarry.eventshuffle.entity.Event + +data class EventResultResponse( + val id: Long, + val name: String, + val suitableDates: List, +) { + companion object { + fun fromEvent(event: Event): EventResultResponse { + if (event.votes.isEmpty()) { + return EventResultResponse( + id = event.id, + name = event.name, + suitableDates = emptyList(), + ) + } + + val allVoters = event.votes.map { it.voterName }.toSet() + + val suitableDates = + event.dates + .filter { date -> + event.votes.all { + date in it.votedDates + } + }.map { date -> + VotesListItemDto( + date = date, + people = allVoters.toList(), + ) + } + + return EventResultResponse( + id = event.id, + name = event.name, + suitableDates = suitableDates, + ) + } + } +} From 4715eb17964992377f8e1e70a32ec8b975eed5ca Mon Sep 17 00:00:00 2001 From: TyostoKarry <114697841+TyostoKarry@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:53:52 +0300 Subject: [PATCH 2/3] Update addVote to not add empty votes - Skip creating a vote entry when no voted dates are provided - Prevents hidden database entries that blocked the results endpoint from showing valid dates --- .../eventshuffle/service/EventService.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/io/github/tyostokarry/eventshuffle/service/EventService.kt b/src/main/kotlin/io/github/tyostokarry/eventshuffle/service/EventService.kt index 1d7326f..94d3a7d 100644 --- a/src/main/kotlin/io/github/tyostokarry/eventshuffle/service/EventService.kt +++ b/src/main/kotlin/io/github/tyostokarry/eventshuffle/service/EventService.kt @@ -40,13 +40,15 @@ class EventService( // If participant has voted previously, remove old vote event.votes.removeIf { it.voterName.equals(request.name, ignoreCase = true) } - val newVote = - EventVote( - voterName = request.name, - votedDates = request.votes, - event = event, - ) - event.votes.add(newVote) + if (request.votes.isNotEmpty()) { + val newVote = + EventVote( + voterName = request.name, + votedDates = request.votes, + event = event, + ) + event.votes.add(newVote) + } val saved = eventRepository.save(event) return saved From 2e70c2d52e2dfdf19f80738e106467fd32a73cac Mon Sep 17 00:00:00 2001 From: TyostoKarry <114697841+TyostoKarry@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:23:41 +0300 Subject: [PATCH 3/3] Add controller tests for getEventResults endpoint - Add unit tests covering /api/v1/event/{id}/results endpoint - Include cases for aggregated results, no votes, no suitable dates, and event not found --- .../controller/EventControllerTest.kt | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/test/kotlin/io/github/tyostokarry/eventshuffle/controller/EventControllerTest.kt b/src/test/kotlin/io/github/tyostokarry/eventshuffle/controller/EventControllerTest.kt index 5ddc077..08708d3 100644 --- a/src/test/kotlin/io/github/tyostokarry/eventshuffle/controller/EventControllerTest.kt +++ b/src/test/kotlin/io/github/tyostokarry/eventshuffle/controller/EventControllerTest.kt @@ -5,6 +5,7 @@ import io.github.tyostokarry.eventshuffle.dto.EventCreateRequest import io.github.tyostokarry.eventshuffle.dto.EventCreateResponse import io.github.tyostokarry.eventshuffle.dto.EventDetailsResponse import io.github.tyostokarry.eventshuffle.dto.EventListResponse +import io.github.tyostokarry.eventshuffle.dto.EventResultResponse import io.github.tyostokarry.eventshuffle.dto.VoteCreateRequest import io.github.tyostokarry.eventshuffle.entity.Event import io.github.tyostokarry.eventshuffle.entity.EventVote @@ -389,4 +390,164 @@ class EventControllerTest( ) } } + + @Nested + inner class GetResult { + @Test + fun `get event results returns aggregated vote summary`() { + val event = + Event( + id = 1L, + name = "Test Event", + dates = + listOf( + LocalDate.of(2025, 10, 5), + LocalDate.of(2025, 10, 6), + ), + ) + + event.votes.addAll( + listOf( + EventVote( + id = 1L, + voterName = "Test Name 1", + votedDates = listOf(LocalDate.of(2025, 10, 5)), + event = event, + ), + EventVote( + id = 2L, + voterName = "Test Name 2", + votedDates = + listOf( + LocalDate.of(2025, 10, 5), + LocalDate.of(2025, 10, 6), + ), + event = event, + ), + ), + ) + + given(eventService.getEventById(1L)).willReturn(event) + + val responseBody = + mockMvc + .get("/api/v1/event/1/results") + .andExpect { status { isOk() } } + .andReturn() + .response + .contentAsString + + val response = objectMapper.readValue(responseBody, EventResultResponse::class.java) + + assertEquals(1, response.id, "Response event id should be 1") + assertEquals("Test Event", response.name, "Response event name should be 'Test Event'") + assertEquals(1, response.suitableDates.size, "Response event should have exactly 1 suitable date") + assertEquals( + LocalDate.of(2025, 10, 5), + response.suitableDates.first().date, + "Response events suitable date should be '2025-10-05'", + ) + assertEquals( + listOf("Test Name 1", "Test Name 2"), + response.suitableDates.first().people, + "Response events suitable date voters should be 'Test Name 1', 'Test Name 2'", + ) + } + + @Test + fun `get event results returns empty suitable dates list when no votes are recorded`() { + val event = + Event( + id = 1L, + name = "Test Event", + dates = + listOf( + LocalDate.of(2025, 10, 5), + LocalDate.of(2025, 10, 6), + ), + ) + + given(eventService.getEventById(1L)).willReturn(event) + + val responseBody = + mockMvc + .get("/api/v1/event/1/results") + .andExpect { status { isOk() } } + .andReturn() + .response + .contentAsString + + val response = objectMapper.readValue(responseBody, EventResultResponse::class.java) + + assertEquals(1, response.id, "Response event id should be 1") + assertEquals("Test Event", response.name, "Response event name should be 'Test Event'") + assertTrue(response.suitableDates.isEmpty(), "Response event should have no suitable dates") + } + + @Test + fun `get event results returns empty suitable dates list when no date has everyone's vote`() { + val event = + Event( + id = 1L, + name = "Test Event", + dates = + listOf( + LocalDate.of(2025, 10, 5), + LocalDate.of(2025, 10, 6), + ), + ) + + event.votes.addAll( + listOf( + EventVote( + id = 1L, + voterName = "Test Name 1", + votedDates = listOf(LocalDate.of(2025, 10, 5)), + event = event, + ), + EventVote( + id = 2L, + voterName = "Test Name 2", + votedDates = listOf(LocalDate.of(2025, 10, 6)), + event = event, + ), + ), + ) + + given(eventService.getEventById(1L)).willReturn(event) + + val responseBody = + mockMvc + .get("/api/v1/event/1/results") + .andExpect { status { isOk() } } + .andReturn() + .response + .contentAsString + + val response = objectMapper.readValue(responseBody, EventResultResponse::class.java) + + assertEquals(1, response.id, "Response event id should be 1") + assertEquals("Test Event", response.name, "Response event name should be 'Test Event'") + assertTrue(response.suitableDates.isEmpty(), "Response event should have no suitable dates") + } + + @Test + fun `get event results returns 404 when event not found`() { + given(eventService.getEventById(999L)).willThrow(EventNotFoundException(999L)) + + val responseBody = + mockMvc + .get("/api/v1/event/999/results") + .andExpect { status { isNotFound() } } + .andReturn() + .response + .contentAsString + + assertEquals( + "Event with ID 999 not found", + responseBody, + "Error body should contain an indication of invalid event id.", + ) + } + } }