diff --git a/src/main/java/com/minitwitter/controller/ExceptionHandlingController.java b/src/main/java/com/minitwitter/controller/ExceptionHandlingController.java new file mode 100644 index 0000000..97fc8e5 --- /dev/null +++ b/src/main/java/com/minitwitter/controller/ExceptionHandlingController.java @@ -0,0 +1,22 @@ +package com.minitwitter.controller; + +import com.minitwitter.domain.dto.ErrorMessage; +import com.minitwitter.service.ExceptionHandlingService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@RestController +@Slf4j +public abstract class ExceptionHandlingController { + + @ExceptionHandler + @ResponseStatus(BAD_REQUEST) + public ErrorMessage handleUnknownUsernameException(ExceptionHandlingService.UnknownUsernameException e) { + log.warn("", e); + return new ErrorMessage(String.format("Unknown user '%s'", e.getUsername())); + } +} diff --git a/src/main/java/com/minitwitter/controller/OverviewController.java b/src/main/java/com/minitwitter/controller/OverviewController.java new file mode 100644 index 0000000..f2fb3b9 --- /dev/null +++ b/src/main/java/com/minitwitter/controller/OverviewController.java @@ -0,0 +1,27 @@ +package com.minitwitter.controller; + +import com.minitwitter.domain.dto.UserOverviewDTO; +import com.minitwitter.service.OverviewService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("overview") +@Slf4j +public class OverviewController extends ExceptionHandlingController{ + + private OverviewService overviewService; + + public OverviewController(OverviewService overviewService) { + this.overviewService = overviewService; + } + + @GetMapping(value = "{username}") + public UserOverviewDTO followers(@PathVariable String username) { + return overviewService.getUsersFollowers(username); + } + +} diff --git a/src/main/java/com/minitwitter/controller/TweetController.java b/src/main/java/com/minitwitter/controller/TweetController.java index 49273cb..fb5667b 100644 --- a/src/main/java/com/minitwitter/controller/TweetController.java +++ b/src/main/java/com/minitwitter/controller/TweetController.java @@ -1,10 +1,9 @@ package com.minitwitter.controller; import com.minitwitter.domain.dto.ErrorMessage; +import com.minitwitter.domain.dto.TweetDTO; import com.minitwitter.service.TweetService; import com.minitwitter.service.TweetService.InvalidTweetException; -import com.minitwitter.service.TweetService.UnknownUsernameException; -import com.minitwitter.domain.dto.TweetDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; @@ -24,7 +23,7 @@ @RestController @RequestMapping("tweets") @Slf4j -public class TweetController { +public class TweetController extends ExceptionHandlingController { private TweetService tweetService; @@ -48,13 +47,6 @@ public Collection tweetsFromUser(@PathVariable String username) { return tweetService.tweetsFromUser(username); } - @ExceptionHandler - @ResponseStatus(BAD_REQUEST) - public ErrorMessage handleUnknownUsernameException(UnknownUsernameException e) { - log.warn("", e); - return new ErrorMessage(String.format("Unknown user '%s'", e.getUsername())); - } - @ExceptionHandler @ResponseStatus(BAD_REQUEST) public ErrorMessage handleInvalidTweetException(InvalidTweetException e) { diff --git a/src/main/java/com/minitwitter/domain/dto/UserOverviewDTO.java b/src/main/java/com/minitwitter/domain/dto/UserOverviewDTO.java new file mode 100644 index 0000000..d6215a4 --- /dev/null +++ b/src/main/java/com/minitwitter/domain/dto/UserOverviewDTO.java @@ -0,0 +1,16 @@ +package com.minitwitter.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PRIVATE; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = PRIVATE) +public class UserOverviewDTO { + private int tweets; + private int followers; + private int following; +} diff --git a/src/main/java/com/minitwitter/repository/TweetRepository.java b/src/main/java/com/minitwitter/repository/TweetRepository.java index 8707d5d..4d622e0 100644 --- a/src/main/java/com/minitwitter/repository/TweetRepository.java +++ b/src/main/java/com/minitwitter/repository/TweetRepository.java @@ -10,4 +10,6 @@ @Repository public interface TweetRepository extends JpaRepository { List findAllByAuthor(User author); + + int countByAuthorUsername(String username); } diff --git a/src/main/java/com/minitwitter/service/ExceptionHandlingService.java b/src/main/java/com/minitwitter/service/ExceptionHandlingService.java new file mode 100644 index 0000000..8fb9918 --- /dev/null +++ b/src/main/java/com/minitwitter/service/ExceptionHandlingService.java @@ -0,0 +1,34 @@ +package com.minitwitter.service; + +import com.minitwitter.domain.User; +import com.minitwitter.repository.UserRepository; +import lombok.Getter; +import org.springframework.stereotype.Service; + +@Service +public abstract class ExceptionHandlingService { + + private UserRepository userRepository; + + public ExceptionHandlingService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + protected User getUser(String username) { + User user = userRepository.findOneByUsername(username); + if (user != null) { + return user; + } + throw new UnknownUsernameException(username); + } + + public static class UnknownUsernameException extends RuntimeException { + @Getter + private String username; + + private UnknownUsernameException(String username) { + super(username); + this.username = username; + } + } +} diff --git a/src/main/java/com/minitwitter/service/OverviewService.java b/src/main/java/com/minitwitter/service/OverviewService.java new file mode 100644 index 0000000..8f5da5a --- /dev/null +++ b/src/main/java/com/minitwitter/service/OverviewService.java @@ -0,0 +1,32 @@ +package com.minitwitter.service; + +import com.minitwitter.domain.User; +import com.minitwitter.domain.dto.UserOverviewDTO; +import com.minitwitter.repository.TweetRepository; +import com.minitwitter.repository.UserRepository; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +public class OverviewService extends ExceptionHandlingService { + + private TweetRepository tweetRepository; + + public OverviewService(TweetRepository tweetRepository, UserRepository userRepository) { + super(userRepository); + this.tweetRepository = tweetRepository; + } + + @Transactional + public UserOverviewDTO getUsersFollowers(String username) { + int tweets = this.tweetRepository.countByAuthorUsername(username); + + User user = this.getUser(username); + int followers = user.getFollowers().size(); + int following = user.getFollowing().size(); + + return new UserOverviewDTO(tweets, followers, following); + } + +} diff --git a/src/main/java/com/minitwitter/service/TweetService.java b/src/main/java/com/minitwitter/service/TweetService.java index ef458f9..52b6ae9 100644 --- a/src/main/java/com/minitwitter/service/TweetService.java +++ b/src/main/java/com/minitwitter/service/TweetService.java @@ -15,14 +15,13 @@ import static java.util.stream.Collectors.toList; @Service -public class TweetService { +public class TweetService extends ExceptionHandlingService { private TweetRepository tweetRepository; - private UserRepository userRepository; public TweetService(TweetRepository tweetRepository, UserRepository userRepository) { + super(userRepository); this.tweetRepository = tweetRepository; - this.userRepository = userRepository; } public Collection followingUsersTweets(Principal principal) { @@ -53,24 +52,6 @@ private List tweetsFromUser(User user) { return tweetRepository.findAllByAuthor(user).stream().map(TweetDTO::new).collect(toList()); } - private User getUser(String username) { - User user = userRepository.findOneByUsername(username); - if (user != null) { - return user; - } - throw new UnknownUsernameException(username); - } - - public static class UnknownUsernameException extends RuntimeException { - @Getter - private String username; - - private UnknownUsernameException(String username) { - super(username); - this.username = username; - } - } - public static class InvalidTweetException extends RuntimeException { private InvalidTweetException(String tweet) { diff --git a/src/test/java/com/minitwitter/controller/OverviewControllerIntegrationTest.java b/src/test/java/com/minitwitter/controller/OverviewControllerIntegrationTest.java new file mode 100644 index 0000000..90452f1 --- /dev/null +++ b/src/test/java/com/minitwitter/controller/OverviewControllerIntegrationTest.java @@ -0,0 +1,37 @@ +package com.minitwitter.controller; + +import com.minitwitter.domain.dto.ErrorMessage; +import com.minitwitter.domain.dto.UserOverviewDTO; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class OverviewControllerIntegrationTest extends RestIntegrationTest { + + @Test + public void overviewOfUser_onlyThatUserTweetsAreReturned() { + ResponseEntity response = withAuthTestRestTemplate().getForEntity("/overview/{username}", + UserOverviewDTO.class, Collections.singletonMap("username", getUsernameOfAuthUser())); + assertThat(response.getStatusCode().is2xxSuccessful(), is(true)); + UserOverviewDTO overview = response.getBody(); + assertThat(overview.getTweets(), equalTo(ownTweetsCount())); + assertThat(overview.getFollowers(), equalTo(followersCount())); + assertThat(overview.getFollowing(), equalTo(followingUsers().length)); + } + + @Test + public void overviewOfUserWithWrongUsername_badRequestReturned() { + ResponseEntity response = withAuthTestRestTemplate().getForEntity("/overview/{username}", + ErrorMessage.class, Collections.singletonMap("username", "unknown")); + assertThat(response.getStatusCode(), is(HttpStatus.BAD_REQUEST)); + assertThat(response.getBody().getMessage(), containsString("unknown")); + } + +} diff --git a/src/test/java/com/minitwitter/controller/RestIntegrationTest.java b/src/test/java/com/minitwitter/controller/RestIntegrationTest.java index 0fd0760..1b92d22 100644 --- a/src/test/java/com/minitwitter/controller/RestIntegrationTest.java +++ b/src/test/java/com/minitwitter/controller/RestIntegrationTest.java @@ -12,18 +12,36 @@ @SpringBootTest(webEnvironment = RANDOM_PORT) public abstract class RestIntegrationTest { + private static final String AUTH_USERNAME = "aantonop"; + private static final int OWN_TWEETS_COUNT = 3; + private static final int FOLLOWERS_COUNT = 1; + private static final String[] FOLLOWING_USERS = new String[]{"rogerkver", "satoshiNakamoto", "SatoshiLite", "VitalikButerin"}; + private static final String[] FOLLOWERS = new String[]{"rogerkver"}; + @Autowired private TestRestTemplate testRestTemplate; TestRestTemplate withAuthTestRestTemplate() { - return testRestTemplate.withBasicAuth("aantonop", "password"); + return testRestTemplate.withBasicAuth(AUTH_USERNAME, "password"); } String getUsernameOfAuthUser() { - return "aantonop"; + return AUTH_USERNAME; } String[] followingUsers() { - return new String[]{"rogerkver", "satoshiNakamoto", "SatoshiLite", "VitalikButerin"}; + return FOLLOWING_USERS; + } + + String[] followers() { + return FOLLOWERS; + } + + int ownTweetsCount() { + return OWN_TWEETS_COUNT; + } + + int followersCount() { + return FOLLOWERS_COUNT; } } diff --git a/src/test/java/com/minitwitter/controller/TweetControllerIntegrationTest.java b/src/test/java/com/minitwitter/controller/TweetControllerIntegrationTest.java index 86cffe3..a00b055 100644 --- a/src/test/java/com/minitwitter/controller/TweetControllerIntegrationTest.java +++ b/src/test/java/com/minitwitter/controller/TweetControllerIntegrationTest.java @@ -45,7 +45,7 @@ public void createTweet_tweetIsSaved() { Tweet fromDb = tweetRepository.findOne(tweet.getId()); assertThat(fromDb, notNullValue()); assertThat(fromDb.getContent(), equalTo(tweet.getContent())); - assertThat(fromDb.getAuthor().getUsername(), equalTo(tweet.getAuthor())); + assertThat(fromDb.getAuthor().getUsername(), equalTo(tweet.getAuthorUsername())); } @Test @@ -76,6 +76,6 @@ public void tweetsFromUserWithWrongUsername_badRequestReturned() { } private List extractAuthorNames(TweetDTO[] body) { - return Arrays.stream(body).map(TweetDTO::getAuthor).distinct().collect(toList()); + return Arrays.stream(body).map(TweetDTO::getAuthorUsername).distinct().collect(toList()); } } diff --git a/src/test/java/com/minitwitter/controller/UserControllerIntegrationTest.java b/src/test/java/com/minitwitter/controller/UserControllerIntegrationTest.java index d3dcac0..e0dceb0 100644 --- a/src/test/java/com/minitwitter/controller/UserControllerIntegrationTest.java +++ b/src/test/java/com/minitwitter/controller/UserControllerIntegrationTest.java @@ -21,8 +21,8 @@ public void followersRequested_allFollowersReturned() { ResponseEntity response = withAuthTestRestTemplate().getForEntity("/followers", UserDTO[].class); assertThat(response.getStatusCode().is2xxSuccessful(), is(true)); List followers = Arrays.asList(response.getBody()); - assertThat(followers, hasSize(1)); - assertThat(extractUsernames(followers), contains("rogerkver")); + assertThat(followers, hasSize(followers().length)); + assertThat(extractUsernames(followers), containsInAnyOrder(followers())); } @Test @@ -30,7 +30,7 @@ public void getFollowingFromFirstPage() { ResponseEntity response = withAuthTestRestTemplate().getForEntity("/following", UserDTO[].class); assertThat(response.getStatusCode().is2xxSuccessful(), is(true)); List following = Arrays.asList(response.getBody()); - assertThat(following, hasSize(4)); + assertThat(following, hasSize(followingUsers().length)); assertThat(extractUsernames(following), containsInAnyOrder(followingUsers())); }