From 999268b06a1ee5da6a12e7403ee98e8b7a380649 Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Sun, 10 Nov 2019 23:48:28 +0100 Subject: [PATCH] Refactor clan creation to be done via Elide POST call --- .../api/clan/ClanControllerTest.java | 2 +- .../com/faforever/api/clan/ClanService.java | 68 ++-- .../faforever/api/clan/ClansController.java | 55 ++- .../com/faforever/api/data/domain/Clan.java | 6 + .../data/listeners/ClanEnricherListener.java | 13 +- .../com/faforever/api/error/ErrorCode.java | 7 +- .../com/faforever/api/map/MapsController.java | 6 +- .../com/faforever/api/mod/ModsController.java | 5 +- .../faforever/api/player/PlayerService.java | 7 +- .../api/voting/VotingController.java | 9 +- .../faforever/api/clan/ClanServiceTest.java | 329 +++++++++++------- .../listeners/ClanEnricherListenerTest.java | 15 +- 12 files changed, 319 insertions(+), 203 deletions(-) diff --git a/src/inttest/java/com/faforever/api/clan/ClanControllerTest.java b/src/inttest/java/com/faforever/api/clan/ClanControllerTest.java index b5d6b0a2f..6cb0923df 100644 --- a/src/inttest/java/com/faforever/api/clan/ClanControllerTest.java +++ b/src/inttest/java/com/faforever/api/clan/ClanControllerTest.java @@ -180,6 +180,6 @@ public void createSecondClan() throws Exception { .andExpect(status().isUnprocessableEntity()) .andReturn(); - assertApiError(result, ErrorCode.CLAN_CREATE_CREATOR_IS_IN_A_CLAN); + assertApiError(result, ErrorCode.CLAN_CREATE_FOUNDER_IS_IN_A_CLAN); } } diff --git a/src/main/java/com/faforever/api/clan/ClanService.java b/src/main/java/com/faforever/api/clan/ClanService.java index b2e3b655b..12de861dd 100644 --- a/src/main/java/com/faforever/api/clan/ClanService.java +++ b/src/main/java/com/faforever/api/clan/ClanService.java @@ -10,43 +10,72 @@ import com.faforever.api.error.ApiException; import com.faforever.api.error.Error; import com.faforever.api.error.ErrorCode; -import com.faforever.api.error.ProgrammingError; -import com.faforever.api.player.PlayerRepository; import com.faforever.api.player.PlayerService; import com.faforever.api.security.JwtService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import org.springframework.security.core.Authentication; import org.springframework.security.jwt.Jwt; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; +import java.util.List; @Service @RequiredArgsConstructor public class ClanService { private final ClanRepository clanRepository; - private final PlayerRepository playerRepository; private final FafApiProperties fafApiProperties; private final JwtService jwtService; private final ObjectMapper objectMapper; private final PlayerService playerService; private final ClanMembershipRepository clanMembershipRepository; + public void preCreate(Clan clan) { + Assert.isNull(clan.getId(), "Clan payload with id can not be used for creation."); + + Player player = playerService.getCurrentPlayer(); + + if (player.getClanMembership() != null) { + throw ApiException.of(ErrorCode.CLAN_CREATE_FOUNDER_IS_IN_A_CLAN); + } + + if (!player.equals(clan.getFounder())) { + throw ApiException.of(ErrorCode.CLAN_INVALID_FOUNDER); + } + + clanRepository.findOneByName(clan.getName()).ifPresent(c -> { + throw ApiException.of(ErrorCode.CLAN_NAME_EXISTS, clan.getName()); + }); + + clanRepository.findOneByTag(clan.getTag()).ifPresent(c -> { + throw ApiException.of(ErrorCode.CLAN_TAG_EXISTS, clan.getTag()); + }); + + clan.setLeader(player); + clan.setMemberships(List.of(new ClanMembership() + .setClan(clan) + .setPlayer(player))); + } + @SneakyThrows - Clan create(String name, String tag, String description, Player creator) { + @Deprecated + // use preCreate instead + Clan create(String name, String tag, String description) { + Player creator = playerService.getCurrentPlayer(); + if (creator.getClanMembership() != null) { - throw new ApiException(new Error(ErrorCode.CLAN_CREATE_CREATOR_IS_IN_A_CLAN)); + throw ApiException.of(ErrorCode.CLAN_CREATE_FOUNDER_IS_IN_A_CLAN); } if (clanRepository.findOneByName(name).isPresent()) { - throw new ApiException(new Error(ErrorCode.CLAN_NAME_EXISTS, name)); + throw ApiException.of(ErrorCode.CLAN_NAME_EXISTS, name); } if (clanRepository.findOneByTag(tag).isPresent()) { - throw new ApiException(new Error(ErrorCode.CLAN_TAG_EXISTS, tag)); + throw ApiException.of(ErrorCode.CLAN_TAG_EXISTS, tag); } Clan clan = new Clan(); @@ -69,16 +98,17 @@ Clan create(String name, String tag, String description, Player creator) { } @SneakyThrows - String generatePlayerInvitationToken(Player requester, int newMemberId, int clanId) { + String generatePlayerInvitationToken(int newMemberId, int clanId) { + Player requester = playerService.getCurrentPlayer(); + Clan clan = clanRepository.findById(clanId) .orElseThrow(() -> new ApiException(new Error(ErrorCode.CLAN_NOT_EXISTS, clanId))); if (requester.getId() != clan.getLeader().getId()) { - throw new ApiException(new Error(ErrorCode.CLAN_NOT_LEADER, clanId)); + throw ApiException.of(ErrorCode.CLAN_NOT_LEADER, clanId); } - Player newMember = playerRepository.findById(newMemberId) - .orElseThrow(() -> new ApiException(new Error(ErrorCode.CLAN_GENERATE_LINK_PLAYER_NOT_FOUND, newMemberId))); + Player newMember = playerService.getById(newMemberId); long expire = Instant.now() .plus(fafApiProperties.getClan().getInviteLinkExpireDurationMinutes(), ChronoUnit.MINUTES) @@ -91,28 +121,26 @@ String generatePlayerInvitationToken(Player requester, int newMemberId, int clan } @SneakyThrows - void acceptPlayerInvitationToken(String stringToken, Authentication authentication) { + void acceptPlayerInvitationToken(String stringToken) { Jwt token = jwtService.decodeAndVerify(stringToken); InvitationResult invitation = objectMapper.readValue(token.getClaims(), InvitationResult.class); if (invitation.isExpired()) { - throw new ApiException(new Error(ErrorCode.CLAN_ACCEPT_TOKEN_EXPIRE)); + throw ApiException.of(ErrorCode.CLAN_ACCEPT_TOKEN_EXPIRE); } final Integer clanId = invitation.getClan().getId(); - Player player = playerService.getPlayer(authentication); + Player player = playerService.getCurrentPlayer(); Clan clan = clanRepository.findById(clanId) .orElseThrow(() -> new ApiException(new Error(ErrorCode.CLAN_NOT_EXISTS, clanId))); - Player newMember = playerRepository.findById(invitation.getNewMember().getId()) - .orElseThrow(() -> new ProgrammingError("ClanMember does not exist: " + invitation.getNewMember().getId())); - + Player newMember = playerService.getById(invitation.getNewMember().getId()); if (player.getId() != newMember.getId()) { - throw new ApiException(new Error(ErrorCode.CLAN_ACCEPT_WRONG_PLAYER)); + throw ApiException.of(ErrorCode.CLAN_ACCEPT_WRONG_PLAYER); } if (newMember.getClan() != null) { - throw new ApiException(new Error(ErrorCode.CLAN_ACCEPT_PLAYER_IN_A_CLAN)); + throw ApiException.of(ErrorCode.CLAN_ACCEPT_PLAYER_IN_A_CLAN); } ClanMembership membership = new ClanMembership(); diff --git a/src/main/java/com/faforever/api/clan/ClansController.java b/src/main/java/com/faforever/api/clan/ClansController.java index 51d316f59..d7050bfd9 100644 --- a/src/main/java/com/faforever/api/clan/ClansController.java +++ b/src/main/java/com/faforever/api/clan/ClansController.java @@ -12,18 +12,16 @@ import io.swagger.annotations.ApiResponses; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.io.IOException; import java.io.Serializable; import java.util.Map; -import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @RestController @@ -37,11 +35,12 @@ public class ClansController { @ApiOperation("Grab data about yourself and the clan") @ApiResponses(value = { - @ApiResponse(code = 200, message = "Success with JSON { player: {id: ?, login: ?}, clan: { id: ?, name: ?, tag: ?}}"), - @ApiResponse(code = 400, message = "Bad Request")}) - @RequestMapping(path = "/me", method = RequestMethod.GET, produces = APPLICATION_JSON_UTF8_VALUE) - public MeResult me(Authentication authentication) { - Player player = playerService.getPlayer(authentication); + @ApiResponse(code = 200, message = "Success with JSON { player: {id: ?, login: ?}, clan: { id: ?, name: ?, tag: ?}}"), + @ApiResponse(code = 400, message = "Bad Request")}) + @RequestMapping(path = "/me", method = RequestMethod.GET, produces = APPLICATION_JSON_VALUE) + @Deprecated // use regular /me route instead + public MeResult me() { + Player player = playerService.getCurrentPlayer(); Clan clan = player.getClan(); ClanResult clanResult = null; @@ -55,44 +54,36 @@ public MeResult me(Authentication authentication) { // a: the new clan with the leader membership, b: the leader membership with the new clan @ApiOperation("Create a clan with correct leader, founder and clan membership") @ApiResponses(value = { - @ApiResponse(code = 200, message = "Success with JSON { id: ?, type: 'clan'}"), - @ApiResponse(code = 400, message = "Bad Request")}) - @RequestMapping(path = "/create", method = RequestMethod.POST, produces = APPLICATION_JSON_UTF8_VALUE) + @ApiResponse(code = 200, message = "Success with JSON { id: ?, type: 'clan'}"), + @ApiResponse(code = 400, message = "Bad Request")}) + @RequestMapping(path = "/create", method = RequestMethod.POST, produces = APPLICATION_JSON_VALUE) @PreAuthorize("hasRole('ROLE_USER')") @Transactional + @Deprecated // use POST /data/clans instead (with a founder in relationships) public Map createClan(@RequestParam(value = "name") String name, @RequestParam(value = "tag") String tag, - @RequestParam(value = "description", required = false) String description, - Authentication authentication) throws IOException { - Player player = playerService.getPlayer(authentication); - Clan clan = clanService.create(name, tag, description, player); + @RequestParam(value = "description", required = false) String description) { + Player player = playerService.getCurrentPlayer(); + Clan clan = clanService.create(name, tag, description); return ImmutableMap.of("id", clan.getId(), "type", "clan"); } @ApiOperation("Generate invitation link") @ApiResponses(value = { - @ApiResponse(code = 200, message = "Success with JSON { jwtToken: ? }"), - @ApiResponse(code = 400, message = "Bad Request")}) - @RequestMapping(path = "/generateInvitationLink", - method = RequestMethod.GET, - produces = APPLICATION_JSON_UTF8_VALUE) + @ApiResponse(code = 200, message = "Success with JSON { jwtToken: ? }"), + @ApiResponse(code = 400, message = "Bad Request")}) + @RequestMapping(path = "/generateInvitationLink", method = RequestMethod.GET, produces = APPLICATION_JSON_VALUE) public Map generateInvitationLink( - @RequestParam(value = "clanId") int clanId, - @RequestParam(value = "playerId") int newMemberId, - Authentication authentication) throws IOException { - Player player = playerService.getPlayer(authentication); - String jwtToken = clanService.generatePlayerInvitationToken(player, newMemberId, clanId); + @RequestParam(value = "clanId") int clanId, + @RequestParam(value = "playerId") int newMemberId) { + String jwtToken = clanService.generatePlayerInvitationToken(newMemberId, clanId); return ImmutableMap.of("jwtToken", jwtToken); } @ApiOperation("Check invitation link and add Member to Clan") - @RequestMapping(path = "/joinClan", - method = RequestMethod.POST, - produces = APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(path = "/joinClan", method = RequestMethod.POST, produces = APPLICATION_JSON_VALUE) @Transactional - public void joinClan( - @RequestParam(value = "token") String stringToken, - Authentication authentication) throws IOException { - clanService.acceptPlayerInvitationToken(stringToken, authentication); + public void joinClan(@RequestParam(value = "token") String stringToken) { + clanService.acceptPlayerInvitationToken(stringToken); } } diff --git a/src/main/java/com/faforever/api/data/domain/Clan.java b/src/main/java/com/faforever/api/data/domain/Clan.java index 78e132baf..7a494700d 100644 --- a/src/main/java/com/faforever/api/data/domain/Clan.java +++ b/src/main/java/com/faforever/api/data/domain/Clan.java @@ -47,6 +47,7 @@ public class Clan extends AbstractEntity implements OwnableEntity { private String description; private String tagColor; private String websiteUrl; + private Boolean requiresInvitation; private List memberships; @Column(name = "name") @@ -88,6 +89,11 @@ public String getTagColor() { return tagColor; } + @Column(name = "requires_invitation", nullable = false) + public Boolean getRequiresInvitation() { + return requiresInvitation; + } + // Cascading is needed for Create & Delete @OneToMany(mappedBy = "clan", cascade = CascadeType.ALL, orphanRemoval = true) // Permission is managed by ClanMembership class diff --git a/src/main/java/com/faforever/api/data/listeners/ClanEnricherListener.java b/src/main/java/com/faforever/api/data/listeners/ClanEnricherListener.java index cc326c822..8f2d29c3a 100644 --- a/src/main/java/com/faforever/api/data/listeners/ClanEnricherListener.java +++ b/src/main/java/com/faforever/api/data/listeners/ClanEnricherListener.java @@ -1,20 +1,31 @@ package com.faforever.api.data.listeners; +import com.faforever.api.clan.ClanService; import com.faforever.api.config.FafApiProperties; import com.faforever.api.data.domain.Clan; import org.springframework.stereotype.Component; import javax.inject.Inject; import javax.persistence.PostLoad; +import javax.persistence.PrePersist; @Component public class ClanEnricherListener { private static FafApiProperties fafApiProperties; + private static ClanService clanService; @Inject - public void init(FafApiProperties fafApiProperties) { + public void init(FafApiProperties fafApiProperties, ClanService clanService) { ClanEnricherListener.fafApiProperties = fafApiProperties; + ClanEnricherListener.clanService = clanService; + } + + @PrePersist + public void prePersist(Clan clan) { + if (clan.getId() == null) { + clanService.preCreate(clan); + } } @PostLoad diff --git a/src/main/java/com/faforever/api/error/ErrorCode.java b/src/main/java/com/faforever/api/error/ErrorCode.java index 9cfe5c131..4621e4ec8 100644 --- a/src/main/java/com/faforever/api/error/ErrorCode.java +++ b/src/main/java/com/faforever/api/error/ErrorCode.java @@ -55,13 +55,13 @@ public enum ErrorCode { INVALID_METADATA(146, "Invalid metadata", "Metadata is not valid: {0}"), MAP_RENAME_FAILED(147, "Cannot rename to correct name failed ", "Cannot rename file ''{0}''"), MAP_INVALID_ZIP(148, "Invalid zip file", "The zip file should only contain one folder at the root level"), - CLAN_CREATE_CREATOR_IS_IN_A_CLAN(149, "You are already in a clan", "Clan creator is already member of a clan"), + CLAN_CREATE_FOUNDER_IS_IN_A_CLAN(149, "You are already in a clan", "Clan creator is already member of a clan"), CLAN_ACCEPT_TOKEN_EXPIRE(150, "Token Expire", "The Invitation Link expire"), CLAN_ACCEPT_WRONG_PLAYER(151, "Wrong Player", "Your are not the invited player"), CLAN_ACCEPT_PLAYER_IN_A_CLAN(152, "Player is in a clan", "You are already in a clan"), CLAN_NOT_LEADER(153, "You Permission", "You are not the leader of the clan"), CLAN_NOT_EXISTS(154, "Cannot find Clan", "Clan with id {0, number} is not available"), - CLAN_GENERATE_LINK_PLAYER_NOT_FOUND(155, "Player not found", "Cannot find player with id {0, number} who should be invited to the clan"), + PLAYER_NOT_FOUND(155, "Player not found", "Cannot find player with id {0, number}."), CLAN_NAME_EXISTS(156, "Clan Name already in use", "The clan name ''{0}'' is already in use. Please choose a different clan name."), CLAN_TAG_EXISTS(157, "Clan Tag already in use", "The clan tag ''{0}'' is already in use. Please choose a different clan tag."), VALIDATION_FAILED(158, "Validation failed", "{0}"), @@ -99,7 +99,8 @@ public enum ErrorCode { PARSING_LUA_FILE_FAILED(189, "Parsing lua files failed", "During the parsing of the lua file an error occured: {0}"), NO_RUSH_RADIUS_MISSING(190, "No rush radius missing", "The scenario file must specify a no rush radius"), INVALID_FEATURED_MOD(191, "Invalid featured mod name", "The featured mod name ''{0}'' is not allowed in this context."), - API_KEY_INVALID(192, "Api key is invalid", "The api key is invalid."); + API_KEY_INVALID(192, "Api key is invalid", "The api key is invalid."), + CLAN_INVALID_FOUNDER(193, "Invalid clan founder", "If you create a clan you must be the founder of it."); private final int code; private final String title; diff --git a/src/main/java/com/faforever/api/map/MapsController.java b/src/main/java/com/faforever/api/map/MapsController.java index 09a3aa00e..9443f459e 100644 --- a/src/main/java/com/faforever/api/map/MapsController.java +++ b/src/main/java/com/faforever/api/map/MapsController.java @@ -14,7 +14,6 @@ import io.swagger.annotations.ApiResponses; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -77,8 +76,7 @@ public void validateScenarioLua(@RequestParam(name = "scenarioLua") String scena @ApiResponse(code = 500, message = "Failure")}) @RequestMapping(path = "/upload", method = RequestMethod.POST, produces = APPLICATION_JSON_UTF8_VALUE) public void uploadMap(@RequestParam("file") MultipartFile file, - @RequestParam("metadata") String jsonString, - Authentication authentication) throws IOException { + @RequestParam("metadata") String jsonString) throws IOException { if (file == null) { throw new ApiException(new Error(ErrorCode.UPLOAD_FILE_MISSING)); } @@ -97,7 +95,7 @@ public void uploadMap(@RequestParam("file") MultipartFile file, throw new ApiException(new Error(ErrorCode.INVALID_METADATA, e.getMessage())); } - Player player = playerService.getPlayer(authentication); + Player player = playerService.getCurrentPlayer(); mapService.uploadMap(file.getInputStream(), file.getOriginalFilename(), player, ranked); } } diff --git a/src/main/java/com/faforever/api/mod/ModsController.java b/src/main/java/com/faforever/api/mod/ModsController.java index abf5e340a..ae1c031be 100644 --- a/src/main/java/com/faforever/api/mod/ModsController.java +++ b/src/main/java/com/faforever/api/mod/ModsController.java @@ -7,7 +7,6 @@ import com.faforever.api.player.PlayerService; import com.google.common.io.Files; import io.swagger.annotations.ApiOperation; -import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -35,7 +34,7 @@ public ModsController(PlayerService playerService, ModService modService, FafApi @ApiOperation("Upload a mod") @RequestMapping(path = "/upload", method = RequestMethod.POST, produces = APPLICATION_JSON_UTF8_VALUE) - public void uploadMod(@RequestParam("file") MultipartFile file, Authentication authentication) throws IOException { + public void uploadMod(@RequestParam("file") MultipartFile file) throws IOException { if (file == null) { throw new ApiException(new Error(ErrorCode.UPLOAD_FILE_MISSING)); } @@ -48,6 +47,6 @@ public void uploadMod(@RequestParam("file") MultipartFile file, Authentication a Path tempFile = java.nio.file.Files.createTempFile("mod", ".tmp"); file.transferTo(tempFile.getFileName().toFile()); - modService.processUploadedMod(tempFile, playerService.getPlayer(authentication)); + modService.processUploadedMod(tempFile, playerService.getCurrentPlayer()); } } diff --git a/src/main/java/com/faforever/api/player/PlayerService.java b/src/main/java/com/faforever/api/player/PlayerService.java index 4517b1dcd..a3f2e92e3 100644 --- a/src/main/java/com/faforever/api/player/PlayerService.java +++ b/src/main/java/com/faforever/api/player/PlayerService.java @@ -7,6 +7,7 @@ import com.faforever.api.security.FafUserDetails; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import static com.faforever.api.error.ErrorCode.TOKEN_INVALID; @@ -17,7 +18,9 @@ public class PlayerService { private final PlayerRepository playerRepository; - public Player getPlayer(Authentication authentication) { + public Player getCurrentPlayer() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() != null && authentication.getPrincipal() instanceof FafUserDetails) { @@ -29,6 +32,6 @@ public Player getPlayer(Authentication authentication) { public Player getById(Integer playerId) { return playerRepository.findById(playerId) - .orElseThrow(() -> new ApiException(new Error(ErrorCode.ENTITY_NOT_FOUND, playerId))); + .orElseThrow(() -> new ApiException(new Error(ErrorCode.PLAYER_NOT_FOUND, playerId))); } } diff --git a/src/main/java/com/faforever/api/voting/VotingController.java b/src/main/java/com/faforever/api/voting/VotingController.java index c61f1e5b4..3c04e1bf3 100644 --- a/src/main/java/com/faforever/api/voting/VotingController.java +++ b/src/main/java/com/faforever/api/voting/VotingController.java @@ -9,7 +9,6 @@ import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -31,14 +30,14 @@ public class VotingController { @ApiOperation(value = "Post a vote") @PreAuthorize("#oauth2.hasScope('" + OAuthScope._VOTE + "')") @RequestMapping(path = "/vote", method = RequestMethod.POST, produces = JsonApiMediaType.JSON_API_MEDIA_TYPE) - public void postVote(@RequestBody Vote vote, Authentication authentication) { - votingService.saveVote(vote, playerService.getPlayer(authentication)); + public void postVote(@RequestBody Vote vote) { + votingService.saveVote(vote, playerService.getCurrentPlayer()); } @ApiOperation(value = "See if user can vote on a subject") @RequestMapping(path = "/votingSubjectsAbleToVote", method = RequestMethod.GET, produces = JsonApiMediaType.JSON_API_MEDIA_TYPE) - public void votingSubjectsAbleTo(HttpServletResponse response, Authentication authentication, HttpServletRequest request) throws IOException { - List votingSubjects = votingService.votingSubjectsAbleToVote(playerService.getPlayer(authentication)); + public void votingSubjectsAbleTo(HttpServletResponse response, HttpServletRequest request) throws IOException { + List votingSubjects = votingService.votingSubjectsAbleToVote(playerService.getCurrentPlayer()); redirectToFilteredVotingSubjects(response, votingSubjects, request); } diff --git a/src/test/java/com/faforever/api/clan/ClanServiceTest.java b/src/test/java/com/faforever/api/clan/ClanServiceTest.java index 692c3d4ce..c856873fe 100644 --- a/src/test/java/com/faforever/api/clan/ClanServiceTest.java +++ b/src/test/java/com/faforever/api/clan/ClanServiceTest.java @@ -8,21 +8,16 @@ import com.faforever.api.data.domain.Player; import com.faforever.api.error.ApiException; import com.faforever.api.error.ErrorCode; -import com.faforever.api.error.ProgrammingError; -import com.faforever.api.player.PlayerRepository; import com.faforever.api.player.PlayerService; import com.faforever.api.security.JwtService; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.migrationsupport.rules.ExpectedExceptionSupport; -import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.jwt.Jwt; @@ -31,22 +26,21 @@ import static com.faforever.api.error.ApiExceptionMatcher.hasErrorCode; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@ExtendWith({MockitoExtension.class, ExpectedExceptionSupport.class}) +@ExtendWith({MockitoExtension.class}) public class ClanServiceTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); @Mock private ClanRepository clanRepository; @Mock - private PlayerRepository playerRepository; - @Mock private FafApiProperties fafApiProperties; @Mock private JwtService jwtService; @@ -54,86 +48,180 @@ public class ClanServiceTest { private PlayerService playerService; @Mock private ClanMembershipRepository clanMembershipRepository; - @Spy private ObjectMapper objectMapper = new ObjectMapper(); - @InjectMocks + private ClanService instance; + @BeforeEach + void setUp() { + instance = new ClanService(clanRepository, fafApiProperties, jwtService, objectMapper, + playerService, clanMembershipRepository); + } + + @Nested + class TestPreCreate { + @Mock + private Clan clan; + @Mock + private Player player; + + @BeforeEach + void setUp() { + when(clan.getId()).thenReturn(null); + when(playerService.getCurrentPlayer()).thenReturn(player); + } + + @Test + void invalidClanCreate() { + reset(clan, playerService); + when(clan.getId()).thenReturn(1); + + assertThrows(IllegalArgumentException.class, () -> instance.preCreate(clan)); + } + + @Test + void founderIsInAClan() { + when(player.getClanMembership()).thenReturn(mock(ClanMembership.class)); + + ApiException e = assertThrows(ApiException.class, () -> instance.preCreate(clan)); + assertThat(e, hasErrorCode(ErrorCode.CLAN_CREATE_FOUNDER_IS_IN_A_CLAN)); + + verify(playerService).getCurrentPlayer(); + } + + @Test + void invalidFounder() { + when(clan.getFounder()).thenReturn(mock(Player.class)); + + ApiException e = assertThrows(ApiException.class, () -> instance.preCreate(clan)); + assertThat(e, hasErrorCode(ErrorCode.CLAN_INVALID_FOUNDER)); + + verify(playerService).getCurrentPlayer(); + } + + @Test + void clanNameExists() { + when(clan.getFounder()).thenReturn(player); + when(clan.getName()).thenReturn("someName"); + when(clanRepository.findOneByName("someName")).thenReturn(Optional.of(mock(Clan.class))); + + ApiException e = assertThrows(ApiException.class, () -> instance.preCreate(clan)); + assertThat(e, hasErrorCode(ErrorCode.CLAN_NAME_EXISTS)); + + verify(playerService).getCurrentPlayer(); + verify(clanRepository).findOneByName("someName"); + } + + @Test + void clanTagExists() { + when(clan.getFounder()).thenReturn(player); + when(clan.getTag()).thenReturn("someTag"); + when(clanRepository.findOneByName(any())).thenReturn(Optional.empty()); + when(clanRepository.findOneByTag("someTag")).thenReturn(Optional.of(mock(Clan.class))); + + ApiException e = assertThrows(ApiException.class, () -> instance.preCreate(clan)); + assertThat(e, hasErrorCode(ErrorCode.CLAN_TAG_EXISTS)); + + verify(playerService).getCurrentPlayer(); + verify(clanRepository).findOneByName(any()); + verify(clanRepository).findOneByTag("someTag"); + } + + @Test + void success() { + reset(clan); + + clan = new Clan(); + clan.setFounder(player); + when(clanRepository.findOneByName(any())).thenReturn(Optional.empty()); + when(clanRepository.findOneByTag(any())).thenReturn(Optional.empty()); + + instance.preCreate(clan); + + assertThat(clan.getFounder(), is(player)); + assertThat(clan.getLeader(), is(player)); + assertThat(clan.getMemberships().size(), is(1)); + assertThat(clan.getMemberships().get(0).getPlayer(), is(player)); + + verify(playerService).getCurrentPlayer(); + verify(clanRepository).findOneByName(any()); + verify(clanRepository).findOneByTag(any()); + } + + } + @Test - public void createClanWhereLeaderIsAlreadyInAClan() { + void createClanWhereLeaderIsAlreadyInAClan() { String clanName = "My cool Clan"; String tag = "123"; String description = "A cool clan for testing"; Player creator = new Player(); creator.setId(1); creator.setClanMembership(new ClanMembership()); - try { - instance.create(clanName, tag, description, creator); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_CREATE_CREATOR_IS_IN_A_CLAN)); - } + when(playerService.getCurrentPlayer()).thenReturn(creator); + + ApiException e = assertThrows(ApiException.class, () -> instance.create(clanName, tag, description)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_CREATE_FOUNDER_IS_IN_A_CLAN)); verify(clanRepository, Mockito.never()).save(any(Clan.class)); } @Test - public void createClanWithSameName() { + void createClanWithSameName() { String clanName = "My cool Clan"; String tag = "123"; String description = "A cool clan for testing"; Player creator = new Player(); creator.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(creator); when(clanRepository.findOneByName(clanName)).thenReturn(Optional.of(new Clan())); - try { - instance.create(clanName, tag, description, creator); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_NAME_EXISTS)); - } + + ApiException e = assertThrows(ApiException.class, () -> instance.create(clanName, tag, description)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_NAME_EXISTS)); ArgumentCaptor clanCaptor = ArgumentCaptor.forClass(Clan.class); verify(clanRepository, Mockito.times(0)).save(clanCaptor.capture()); } @Test - public void createClanWithSameTag() { + void createClanWithSameTag() { String clanName = "My cool Clan"; String tag = "123"; String description = "A cool clan for testing"; Player creator = new Player(); creator.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(creator); when(clanRepository.findOneByName(clanName)).thenReturn(Optional.empty()); when(clanRepository.findOneByTag(tag)).thenReturn(Optional.of(new Clan())); - try { - instance.create(clanName, tag, description, creator); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_TAG_EXISTS)); - } + ApiException e = assertThrows(ApiException.class, () -> instance.create(clanName, tag, description)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_TAG_EXISTS)); ArgumentCaptor clanCaptor = ArgumentCaptor.forClass(Clan.class); verify(clanRepository, Mockito.times(0)).save(clanCaptor.capture()); } @Test - public void createClanSuccessful() { + void createClanSuccessful() { String clanName = "My cool Clan"; String tag = "123"; String description = "A cool clan for testing"; Player creator = new Player(); creator.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(creator); when(clanRepository.findOneByName(clanName)).thenReturn(Optional.empty()); when(clanRepository.findOneByTag(tag)).thenReturn(Optional.empty()); - instance.create(clanName, tag, description, creator); + instance.create(clanName, tag, description); ArgumentCaptor clanCaptor = ArgumentCaptor.forClass(Clan.class); verify(clanRepository, Mockito.times(1)).save(clanCaptor.capture()); assertEquals(clanName, clanCaptor.getValue().getName()); @@ -146,23 +234,22 @@ public void createClanSuccessful() { } @Test - public void generatePlayerInvitationTokenWithInvalidClan() throws IOException { + void generatePlayerInvitationTokenWithInvalidClan() throws IOException { Player requester = new Player(); requester.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(requester); - try { - instance.generatePlayerInvitationToken(requester, 45, 42); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_NOT_EXISTS)); - } + ApiException e = assertThrows(ApiException.class, () -> instance.generatePlayerInvitationToken(45, 42)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_NOT_EXISTS)); verify(jwtService, Mockito.never()).sign(any()); } @Test - public void generatePlayerInvitationTokenFromNonLeader() throws IOException { + void generatePlayerInvitationTokenFromNonLeader() throws IOException { Player requester = new Player(); requester.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(requester); Player newMember = new Player(); newMember.setId(2); @@ -170,41 +257,38 @@ public void generatePlayerInvitationTokenFromNonLeader() throws IOException { Player leader = new Player(); leader.setId(3); - Clan clan = ClanFactory.builder().leader(leader).build(); + Clan clan = ClanFactory.builder().id(42).leader(leader).build(); when(clanRepository.findById(clan.getId())).thenReturn(Optional.of(clan)); - try { - instance.generatePlayerInvitationToken(requester, newMember.getId(), clan.getId()); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_NOT_LEADER)); - } + ApiException e = assertThrows(ApiException.class, () -> instance.generatePlayerInvitationToken(45, 42)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_NOT_LEADER)); verify(jwtService, Mockito.never()).sign(any()); } @Test - public void generatePlayerInvitationTokenInvalidPlayer() throws IOException { + void generatePlayerInvitationTokenInvalidPlayer() throws IOException { Player requester = new Player(); requester.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(requester); - Clan clan = ClanFactory.builder().leader(requester).build(); + Clan clan = ClanFactory.builder().id(42).leader(requester).build(); when(clanRepository.findById(clan.getId())).thenReturn(Optional.of(clan)); + when(playerService.getById(45)).thenThrow(ApiException.of(ErrorCode.PLAYER_NOT_FOUND)); - try { - instance.generatePlayerInvitationToken(requester, 42, clan.getId()); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_GENERATE_LINK_PLAYER_NOT_FOUND)); - } + ApiException e = assertThrows(ApiException.class, () -> instance.generatePlayerInvitationToken(45, 42)); + + assertThat(e, hasErrorCode(ErrorCode.PLAYER_NOT_FOUND)); verify(jwtService, Mockito.never()).sign(any()); } @Test - public void generatePlayerInvitationToken() throws IOException { + void generatePlayerInvitationToken() throws IOException { Player requester = new Player(); requester.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(requester); Player newMember = new Player(); newMember.setId(2); @@ -214,15 +298,15 @@ public void generatePlayerInvitationToken() throws IOException { FafApiProperties props = new FafApiProperties(); when(clanRepository.findById(clan.getId())).thenReturn(Optional.of(clan)); - when(playerRepository.findById(newMember.getId())).thenReturn(Optional.of(newMember)); + when(playerService.getById(newMember.getId())).thenReturn(newMember); when(fafApiProperties.getClan()).thenReturn(props.getClan()); - instance.generatePlayerInvitationToken(requester, newMember.getId(), clan.getId()); + instance.generatePlayerInvitationToken(newMember.getId(), clan.getId()); ArgumentCaptor captor = ArgumentCaptor.forClass(InvitationResult.class); verify(jwtService, Mockito.times(1)).sign(captor.capture()); assertThat("expire", - captor.getValue().getExpire(), - greaterThan(System.currentTimeMillis())); + captor.getValue().getExpire(), + greaterThan(System.currentTimeMillis())); assertEquals(newMember.getId(), captor.getValue().getNewMember().getId()); assertEquals(newMember.getLogin(), captor.getValue().getNewMember().getLogin()); assertEquals(clan.getId(), captor.getValue().getClan().getId()); @@ -231,70 +315,65 @@ public void generatePlayerInvitationToken() throws IOException { } @Test - public void acceptPlayerInvitationTokenExpire() throws IOException { + void acceptPlayerInvitationTokenExpire() throws IOException { String stringToken = "1234"; long expire = System.currentTimeMillis(); - Jwt jwtToken = Mockito.mock(Jwt.class); + Jwt jwtToken = mock(Jwt.class); when(jwtToken.getClaims()).thenReturn( - String.format("{\"expire\":%s}", expire)); + String.format("{\"expire\":%s}", expire)); when(jwtService.decodeAndVerify(any())).thenReturn(jwtToken); - try { - instance.acceptPlayerInvitationToken(stringToken, null); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_ACCEPT_TOKEN_EXPIRE)); - } + ApiException e = assertThrows(ApiException.class, () -> instance.acceptPlayerInvitationToken(stringToken)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_ACCEPT_TOKEN_EXPIRE)); verify(clanMembershipRepository, Mockito.never()).save(any(ClanMembership.class)); } @Test - public void acceptPlayerInvitationTokenInvalidClan() throws IOException { + void acceptPlayerInvitationTokenInvalidClan() throws IOException { String stringToken = "1234"; long expire = System.currentTimeMillis() + 1000 * 3; - Jwt jwtToken = Mockito.mock(Jwt.class); + Jwt jwtToken = mock(Jwt.class); when(jwtToken.getClaims()).thenReturn( - String.format("{\"expire\":%s,\"clan\":{\"id\":42}}", expire)); + String.format("{\"expire\":%s,\"clan\":{\"id\":42}}", expire)); when(jwtService.decodeAndVerify(any())).thenReturn(jwtToken); - try { - instance.acceptPlayerInvitationToken(stringToken, null); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_NOT_EXISTS)); - } + ApiException e = assertThrows(ApiException.class, () -> instance.acceptPlayerInvitationToken(stringToken)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_NOT_EXISTS)); verify(clanMembershipRepository, Mockito.never()).save(any(ClanMembership.class)); } - @Test - public void acceptPlayerInvitationTokenInvalidPlayer() throws IOException { + void acceptPlayerInvitationTokenInvalidPlayer() throws IOException { + Player requester = new Player(); + requester.setId(1); + when(playerService.getCurrentPlayer()).thenReturn(requester); + String stringToken = "1234"; Clan clan = ClanFactory.builder().build(); long expire = System.currentTimeMillis() + 1000 * 3; - Jwt jwtToken = Mockito.mock(Jwt.class); + Jwt jwtToken = mock(Jwt.class); when(jwtToken.getClaims()).thenReturn( - String.format("{\"expire\":%s,\"newMember\":{\"id\":2},\"clan\":{\"id\":%s}}", - expire, clan.getId())); + String.format("{\"expire\":%s,\"newMember\":{\"id\":2},\"clan\":{\"id\":%s}}", + expire, clan.getId())); when(jwtService.decodeAndVerify(any())).thenReturn(jwtToken); when(clanRepository.findById(clan.getId())).thenReturn(Optional.of(clan)); + when(playerService.getById(2)).thenThrow(ApiException.of(ErrorCode.PLAYER_NOT_FOUND)); - try { - instance.acceptPlayerInvitationToken(stringToken, null); - fail(); - } catch (ProgrammingError e) { - assertEquals("ClanMember does not exist: 2", e.getMessage()); - } + ApiException e = assertThrows(ApiException.class, () -> instance.acceptPlayerInvitationToken(stringToken)); + + assertThat(e, hasErrorCode(ErrorCode.PLAYER_NOT_FOUND)); verify(clanMembershipRepository, Mockito.never()).save(any(ClanMembership.class)); } @Test - public void acceptPlayerInvitationTokenWrongPlayer() throws IOException { + void acceptPlayerInvitationTokenWrongPlayer() throws IOException { String stringToken = "1234"; Player newMember = new Player(); @@ -306,27 +385,24 @@ public void acceptPlayerInvitationTokenWrongPlayer() throws IOException { otherPlayer.setId(3); long expire = System.currentTimeMillis() + 1000 * 3; - Jwt jwtToken = Mockito.mock(Jwt.class); + Jwt jwtToken = mock(Jwt.class); when(jwtToken.getClaims()).thenReturn( - String.format("{\"expire\":%s,\"newMember\":{\"id\":%s},\"clan\":{\"id\":%s}}", - expire, newMember.getId(), clan.getId())); + String.format("{\"expire\":%s,\"newMember\":{\"id\":%s},\"clan\":{\"id\":%s}}", + expire, newMember.getId(), clan.getId())); when(jwtService.decodeAndVerify(any())).thenReturn(jwtToken); when(clanRepository.findById(clan.getId())).thenReturn(Optional.of(clan)); - when(playerRepository.findById(newMember.getId())).thenReturn(Optional.of(newMember)); - when(playerService.getPlayer(any())).thenReturn(otherPlayer); - - try { - instance.acceptPlayerInvitationToken(stringToken, null); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_ACCEPT_WRONG_PLAYER)); - } + when(playerService.getById(newMember.getId())).thenReturn(newMember); + when(playerService.getCurrentPlayer()).thenReturn(otherPlayer); + + ApiException e = assertThrows(ApiException.class, () -> instance.acceptPlayerInvitationToken(stringToken)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_ACCEPT_WRONG_PLAYER)); verify(clanMembershipRepository, Mockito.never()).save(any(ClanMembership.class)); } @Test - public void acceptPlayerInvitationTokenPlayerIAlreadyInAClan() throws IOException { + void acceptPlayerInvitationTokenPlayerIAlreadyInAClan() throws IOException { String stringToken = "1234"; Clan clan = ClanFactory.builder().build(); @@ -336,43 +412,40 @@ public void acceptPlayerInvitationTokenPlayerIAlreadyInAClan() throws IOExceptio newMember.setClanMembership(new ClanMembership().setClan(clan).setPlayer(newMember)); long expire = System.currentTimeMillis() + 1000 * 3; - Jwt jwtToken = Mockito.mock(Jwt.class); + Jwt jwtToken = mock(Jwt.class); when(jwtToken.getClaims()).thenReturn( - String.format("{\"expire\":%s,\"newMember\":{\"id\":%s},\"clan\":{\"id\":%s}}", - expire, newMember.getId(), clan.getId())); + String.format("{\"expire\":%s,\"newMember\":{\"id\":%s},\"clan\":{\"id\":%s}}", + expire, newMember.getId(), clan.getId())); when(jwtService.decodeAndVerify(any())).thenReturn(jwtToken); when(clanRepository.findById(clan.getId())).thenReturn(Optional.of(clan)); - when(playerRepository.findById(newMember.getId())).thenReturn(Optional.of(newMember)); - when(playerService.getPlayer(any())).thenReturn(newMember); - - try { - instance.acceptPlayerInvitationToken(stringToken, null); - fail(); - } catch (ApiException e) { - assertThat(e, hasErrorCode(ErrorCode.CLAN_ACCEPT_PLAYER_IN_A_CLAN)); - } + when(playerService.getById(newMember.getId())).thenReturn(newMember); + when(playerService.getCurrentPlayer()).thenReturn(newMember); + + ApiException e = assertThrows(ApiException.class, () -> instance.acceptPlayerInvitationToken(stringToken)); + + assertThat(e, hasErrorCode(ErrorCode.CLAN_ACCEPT_PLAYER_IN_A_CLAN)); verify(clanMembershipRepository, Mockito.never()).save(any(ClanMembership.class)); } @Test - public void acceptPlayerInvitationToken() throws IOException { + void acceptPlayerInvitationToken() throws IOException { String stringToken = "1234"; Clan clan = ClanFactory.builder().build(); Player newMember = new Player(); newMember.setId(2); long expire = System.currentTimeMillis() + 1000 * 3; - Jwt jwtToken = Mockito.mock(Jwt.class); + Jwt jwtToken = mock(Jwt.class); when(jwtToken.getClaims()).thenReturn( - String.format("{\"expire\":%s,\"newMember\":{\"id\":%s},\"clan\":{\"id\":%s}}", - expire, newMember.getId(), clan.getId())); + String.format("{\"expire\":%s,\"newMember\":{\"id\":%s},\"clan\":{\"id\":%s}}", + expire, newMember.getId(), clan.getId())); when(jwtService.decodeAndVerify(any())).thenReturn(jwtToken); when(clanRepository.findById(clan.getId())).thenReturn(Optional.of(clan)); - when(playerRepository.findById(newMember.getId())).thenReturn(Optional.of(newMember)); - when(playerService.getPlayer(any())).thenReturn(newMember); + when(playerService.getById(newMember.getId())).thenReturn(newMember); + when(playerService.getCurrentPlayer()).thenReturn(newMember); - instance.acceptPlayerInvitationToken(stringToken, null); + instance.acceptPlayerInvitationToken(stringToken); ArgumentCaptor captor = ArgumentCaptor.forClass(ClanMembership.class); verify(clanMembershipRepository, Mockito.times(1)).save(captor.capture()); diff --git a/src/test/java/com/faforever/api/data/listeners/ClanEnricherListenerTest.java b/src/test/java/com/faforever/api/data/listeners/ClanEnricherListenerTest.java index f77a8f2e9..c51a38152 100644 --- a/src/test/java/com/faforever/api/data/listeners/ClanEnricherListenerTest.java +++ b/src/test/java/com/faforever/api/data/listeners/ClanEnricherListenerTest.java @@ -1,30 +1,37 @@ package com.faforever.api.data.listeners; import com.faforever.api.clan.ClanFactory; +import com.faforever.api.clan.ClanService; import com.faforever.api.config.FafApiProperties; import com.faforever.api.data.domain.Clan; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +@ExtendWith(MockitoExtension.class) public class ClanEnricherListenerTest { + @Mock + private ClanService clanService; + private ClanEnricherListener instance; @BeforeEach - public void setUp() throws Exception { + void setUp() throws Exception { instance = new ClanEnricherListener(); FafApiProperties fafApiProperties = new FafApiProperties(); - instance.init(fafApiProperties); - + instance.init(fafApiProperties, clanService); fafApiProperties.getClan().setWebsiteUrlFormat("http://example.com/%s"); } @Test - public void enrich() throws Exception { + void enrich() throws Exception { Clan clan = ClanFactory.builder().id(54).build(); instance.enrich(clan);