diff --git a/src/inttest/java/com/faforever/api/event/EventControllerTest.java b/src/inttest/java/com/faforever/api/event/EventControllerTest.java new file mode 100644 index 000000000..3cd3b1989 --- /dev/null +++ b/src/inttest/java/com/faforever/api/event/EventControllerTest.java @@ -0,0 +1,112 @@ +package com.faforever.api.event; + +import com.faforever.api.AbstractIntegrationTest; +import com.faforever.api.data.DataController; +import com.faforever.api.security.OAuthScope; +import com.google.common.collect.Lists; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; + +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepDefaultUser.sql") +@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/prepEventsData.sql") +@Sql(executionPhase = ExecutionPhase.AFTER_TEST_METHOD, scripts = "classpath:sql/cleanEventsData.sql") +public class EventControllerTest extends AbstractIntegrationTest { + + @Test + public void singleExistingPlayerEventCanBeUpdated() throws Exception { + List updatedEvents = Lists.newArrayList(new EventUpdateRequest(1, "15b6c19a-6084-4e82-ada9-6c30e282191f", 10)); + mockMvc.perform( + patch("/events/update") + .header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE) + .content(objectMapper.writeValueAsString(updatedEvents)) + .with(getOAuthToken(OAuthScope._WRITE_EVENTS))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data", hasSize(1))) + .andExpect(jsonPath("$.data[0].attributes.currentCount", Matchers.is(31))) + .andExpect(jsonPath("$.data[0].attributes.eventId", Matchers.is("15b6c19a-6084-4e82-ada9-6c30e282191f"))); + } + + @Test + public void singleNonExistingPlayerEventCanBeCreated() throws Exception { + List updatedEvents = Lists.newArrayList(new EventUpdateRequest(1, "cc791f00-343c-48d4-b5b3-8900b83209c0", 10)); + mockMvc.perform( + patch("/events/update") + .header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE) + .content(objectMapper.writeValueAsString(updatedEvents)) + .with(getOAuthToken(OAuthScope._WRITE_EVENTS))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data", hasSize(1))) + .andExpect(jsonPath("$.data[0].attributes.currentCount", Matchers.is(10))) + .andExpect(jsonPath("$.data[0].attributes.eventId", Matchers.is("cc791f00-343c-48d4-b5b3-8900b83209c0"))); + } + + @Test + public void multipleExistingPlayerEventsCanBeUpdated() throws Exception { + List updatedEvents = Lists.newArrayList( + new EventUpdateRequest(1, "15b6c19a-6084-4e82-ada9-6c30e282191f", 10), + new EventUpdateRequest(1, "225e9b2e-ae09-4ae1-a198-eca8780b0fcd", 10) + ); + mockMvc.perform( + patch("/events/update") + .header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE) + .content(objectMapper.writeValueAsString(updatedEvents)) + .with(getOAuthToken(OAuthScope._WRITE_EVENTS))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data", hasSize(2))) + .andExpect(jsonPath("$.data[*].attributes.currentCount", Matchers.containsInAnyOrder(31, 20))) + .andExpect(jsonPath("$.data[*].attributes.eventId", Matchers.containsInAnyOrder("15b6c19a-6084-4e82-ada9-6c30e282191f", "225e9b2e-ae09-4ae1-a198-eca8780b0fcd"))); + } + + @Test + public void authWithoutCorrectScopeShouldFail() throws Exception { + List updatedEvents = Lists.newArrayList(new EventUpdateRequest(1, "15b6c19a-6084-4e82-ada9-6c30e282191f", 10)); + mockMvc.perform( + patch("/events/update") + .header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE) + .content(objectMapper.writeValueAsString(updatedEvents))) + .andExpect(status().isForbidden()); + } + + @Test + public void nonExistingEventShouldFail() throws Exception { + List updatedEvents = Lists.newArrayList(new EventUpdateRequest(1, "non-existing-event-id", 10)); + mockMvc.perform( + patch("/events/update") + .header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE) + .content(objectMapper.writeValueAsString(updatedEvents)) + .with(getOAuthToken(OAuthScope._WRITE_EVENTS))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void nonExistingPlayerShouldFail() throws Exception { + List updatedEvents = Lists.newArrayList(new EventUpdateRequest(-1, "15b6c19a-6084-4e82-ada9-6c30e282191f", 10)); + mockMvc.perform( + patch("/events/update") + .header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE) + .content(objectMapper.writeValueAsString(updatedEvents)) + .with(getOAuthToken(OAuthScope._WRITE_EVENTS))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void negativeCountShouldFail() throws Exception { + List updatedEvents = Lists.newArrayList(new EventUpdateRequest(1, "15b6c19a-6084-4e82-ada9-6c30e282191f", -10)); + mockMvc.perform( + patch("/events/update") + .header(HttpHeaders.CONTENT_TYPE, DataController.JSON_API_MEDIA_TYPE) + .content(objectMapper.writeValueAsString(updatedEvents)) + .with(getOAuthToken(OAuthScope._WRITE_EVENTS))) + .andExpect(status().isUnprocessableEntity()); + } +} diff --git a/src/inttest/resources/sql/cleanEventsData.sql b/src/inttest/resources/sql/cleanEventsData.sql new file mode 100644 index 000000000..107b6dc77 --- /dev/null +++ b/src/inttest/resources/sql/cleanEventsData.sql @@ -0,0 +1,4 @@ +DELETE +FROM player_events; +DELETE +FROM event_definitions; diff --git a/src/inttest/resources/sql/prepEventsData.sql b/src/inttest/resources/sql/prepEventsData.sql new file mode 100644 index 000000000..b01dea7f1 --- /dev/null +++ b/src/inttest/resources/sql/prepEventsData.sql @@ -0,0 +1,14 @@ +DELETE +FROM player_events; +DELETE +FROM event_definitions; +INSERT INTO event_definitions (id, name_key, image_url, type) +VALUES ('15b6c19a-6084-4e82-ada9-6c30e282191f', 'event.seraphimWins', null, 'NUMERIC'); +INSERT INTO event_definitions (id, name_key, image_url, type) +VALUES ('225e9b2e-ae09-4ae1-a198-eca8780b0fcd', 'event.lostAirUnits', null, 'NUMERIC'); +INSERT INTO event_definitions (id, name_key, image_url, type) +VALUES ('cc791f00-343c-48d4-b5b3-8900b83209c0', 'event.secondsPlayed', null, 'TIME'); +INSERT INTO player_events (id, player_id, event_id, count, create_time, update_time) +VALUES (1, 1, '15b6c19a-6084-4e82-ada9-6c30e282191f', 21, '2019-01-06 10:48:18', '2019-01-06 10:48:18'); +INSERT INTO player_events (id, player_id, event_id, count, create_time, update_time) +VALUES (2, 1, '225e9b2e-ae09-4ae1-a198-eca8780b0fcd', 10, '2019-01-06 13:36:54', '2019-01-06 13:36:54'); diff --git a/src/main/java/com/faforever/api/data/domain/PlayerEvent.java b/src/main/java/com/faforever/api/data/domain/PlayerEvent.java index e7a15b391..7098bbc91 100644 --- a/src/main/java/com/faforever/api/data/domain/PlayerEvent.java +++ b/src/main/java/com/faforever/api/data/domain/PlayerEvent.java @@ -36,7 +36,7 @@ public int getPlayerId() { } @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "event_id", updatable = false, insertable = false) + @JoinColumn(name = "event_id", updatable = false) public Event getEvent() { return event; } diff --git a/src/main/java/com/faforever/api/event/EventUpdateRequest.java b/src/main/java/com/faforever/api/event/EventUpdateRequest.java index eeed5171c..9fbd166f3 100644 --- a/src/main/java/com/faforever/api/event/EventUpdateRequest.java +++ b/src/main/java/com/faforever/api/event/EventUpdateRequest.java @@ -5,6 +5,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import javax.validation.constraints.Min; + @Getter @Setter @NoArgsConstructor @@ -13,6 +15,7 @@ class EventUpdateRequest { private int playerId; private String eventId; + @Min(0) private int count; } diff --git a/src/main/java/com/faforever/api/event/EventsController.java b/src/main/java/com/faforever/api/event/EventsController.java index 7ab8df324..9251d66c5 100644 --- a/src/main/java/com/faforever/api/event/EventsController.java +++ b/src/main/java/com/faforever/api/event/EventsController.java @@ -8,35 +8,35 @@ import com.yahoo.elide.jsonapi.models.Resource; import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.inject.Inject; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicInteger; +import javax.validation.Valid; +import java.util.List; import java.util.stream.Collectors; @RestController @RequestMapping(path = "/events") +@Validated public class EventsController { private static final String JSON_API_MEDIA_TYPE = "application/vnd.api+json"; private final EventsService eventsService; - private AtomicInteger nextUpdateId; @Inject public EventsController(EventsService eventsService) { this.eventsService = eventsService; - nextUpdateId = new AtomicInteger(); } @ApiOperation(value = "Updates the state and progress of one or multiple events.") @PreAuthorize("#oauth2.hasScope('" + OAuthScope._WRITE_EVENTS + "')") @RequestMapping(value = "/update", method = RequestMethod.PATCH, produces = JSON_API_MEDIA_TYPE) - public JsonApiDocument update(@RequestBody EventUpdateRequest[] updateRequests) { - return new JsonApiDocument(new Data<>(Arrays.stream(updateRequests) + public JsonApiDocument update(@RequestBody List<@Valid EventUpdateRequest> updateRequests) { + return new JsonApiDocument(new Data<>(updateRequests.stream() .map(request -> eventsService.increment(request.getPlayerId(), request.getEventId(), request.getCount())) .map(this::toResource) .collect(Collectors.toList()))); @@ -47,7 +47,7 @@ private Resource toResource(UpdatedEventResponse updatedEventResponse) { .put("eventId", updatedEventResponse.getEventId()) .put("currentCount", updatedEventResponse.getCurrentCount()); - return new Resource("updatedEvent", String.valueOf(nextUpdateId.getAndIncrement()), + return new Resource("updatedEvent", String.valueOf(updatedEventResponse.getId()), attributesBuilder.build(), null, null, null); } } diff --git a/src/main/java/com/faforever/api/event/EventsService.java b/src/main/java/com/faforever/api/event/EventsService.java index 0327e04d8..8adf0c2fc 100644 --- a/src/main/java/com/faforever/api/event/EventsService.java +++ b/src/main/java/com/faforever/api/event/EventsService.java @@ -2,6 +2,10 @@ import com.faforever.api.data.domain.Event; import com.faforever.api.data.domain.PlayerEvent; +import com.faforever.api.error.ApiException; +import com.faforever.api.error.Error; +import com.faforever.api.error.ErrorCode; +import com.faforever.api.player.PlayerService; import com.google.common.base.MoreObjects; import org.springframework.stereotype.Service; @@ -12,17 +16,21 @@ public class EventsService { private final EventRepository eventRepository; + private final PlayerService playerService; private final PlayerEventRepository playerEventRepository; @Inject - public EventsService(EventRepository eventRepository, PlayerEventRepository playerEventRepository) { + public EventsService(EventRepository eventRepository, PlayerService playerService, PlayerEventRepository playerEventRepository) { this.eventRepository = eventRepository; + this.playerService = playerService; this.playerEventRepository = playerEventRepository; } UpdatedEventResponse increment(int playerId, String eventId, int steps) { BiFunction stepsFunction = (currentSteps, newSteps) -> currentSteps + newSteps; - Event event = eventRepository.getOne(eventId); + playerService.getById(playerId); + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new ApiException(new Error(ErrorCode.ENTITY_NOT_FOUND, eventId))); PlayerEvent playerEvent = getOrCreatePlayerEvent(playerId, event); @@ -32,7 +40,7 @@ UpdatedEventResponse increment(int playerId, String eventId, int steps) { playerEvent.setCurrentCount(newCurrentCount); playerEventRepository.save(playerEvent); - return new UpdatedEventResponse(eventId, newCurrentCount); + return new UpdatedEventResponse(playerEvent.getId(), eventId, newCurrentCount); } private PlayerEvent getOrCreatePlayerEvent(int playerId, Event event) { diff --git a/src/main/java/com/faforever/api/event/UpdatedEventResponse.java b/src/main/java/com/faforever/api/event/UpdatedEventResponse.java index 7564eff11..54676e1bc 100644 --- a/src/main/java/com/faforever/api/event/UpdatedEventResponse.java +++ b/src/main/java/com/faforever/api/event/UpdatedEventResponse.java @@ -5,6 +5,7 @@ @Data class UpdatedEventResponse { + private final int id; private final String eventId; private final Integer currentCount; diff --git a/src/main/java/com/faforever/api/player/PlayerService.java b/src/main/java/com/faforever/api/player/PlayerService.java index d0a64cc9c..c9b378a12 100644 --- a/src/main/java/com/faforever/api/player/PlayerService.java +++ b/src/main/java/com/faforever/api/player/PlayerService.java @@ -3,6 +3,7 @@ import com.faforever.api.data.domain.Player; import com.faforever.api.error.ApiException; import com.faforever.api.error.Error; +import com.faforever.api.error.ErrorCode; import com.faforever.api.security.FafUserDetails; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; @@ -30,4 +31,9 @@ public Player getPlayer(Authentication authentication) { } throw new ApiException(new Error(TOKEN_INVALID)); } + + public Player getById(Integer playerId) { + return playerRepository.findById(playerId) + .orElseThrow(() -> new ApiException(new Error(ErrorCode.ENTITY_NOT_FOUND, playerId))); + } } diff --git a/src/test/java/com/faforever/api/achievements/PlayerEventsControllerTest.java b/src/test/java/com/faforever/api/achievements/PlayerAchievementsControllerTest.java similarity index 98% rename from src/test/java/com/faforever/api/achievements/PlayerEventsControllerTest.java rename to src/test/java/com/faforever/api/achievements/PlayerAchievementsControllerTest.java index 60a1e5a46..4769f8446 100644 --- a/src/test/java/com/faforever/api/achievements/PlayerEventsControllerTest.java +++ b/src/test/java/com/faforever/api/achievements/PlayerAchievementsControllerTest.java @@ -18,7 +18,7 @@ @RunWith(MockitoJUnitRunner.class) -public class PlayerEventsControllerTest { +public class PlayerAchievementsControllerTest { private AchievementsController instance;