diff --git a/.gitignore b/.gitignore index c9f7010..4c6eb77 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ build/ nbbuild/ dist/ nbdist/ -.nb-gradle/ \ No newline at end of file +.nb-gradle/ +/bin/ diff --git a/src/main/java/com/scmspain/MsFcTechTestApplication.java b/src/main/java/com/scmspain/MsFcTechTestApplication.java index 28b3538..ba2b462 100644 --- a/src/main/java/com/scmspain/MsFcTechTestApplication.java +++ b/src/main/java/com/scmspain/MsFcTechTestApplication.java @@ -1,13 +1,13 @@ package com.scmspain; -import com.scmspain.configuration.InfrastructureConfiguration; -import com.scmspain.configuration.TweetConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import com.scmspain.configuration.InfrastructureConfiguration; +import com.scmspain.configuration.TweetConfiguration; + @Configuration @EnableAutoConfiguration @Import({TweetConfiguration.class, InfrastructureConfiguration.class}) diff --git a/src/main/java/com/scmspain/configuration/TweetConfiguration.java b/src/main/java/com/scmspain/configuration/TweetConfiguration.java index fbb0dbd..4e74d6b 100644 --- a/src/main/java/com/scmspain/configuration/TweetConfiguration.java +++ b/src/main/java/com/scmspain/configuration/TweetConfiguration.java @@ -1,22 +1,25 @@ package com.scmspain.configuration; -import com.scmspain.controller.TweetController; -import com.scmspain.services.TweetService; import org.springframework.boot.actuate.metrics.writer.MetricWriter; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import javax.persistence.EntityManager; +import com.scmspain.controller.TweetController; +import com.scmspain.repository.TweetRepository; +import com.scmspain.services.TweetService; @Configuration +@ComponentScan(basePackages = "com.scmspain.exceptions") public class TweetConfiguration { @Bean - public TweetService getTweetService(EntityManager entityManager, MetricWriter metricWriter) { - return new TweetService(entityManager, metricWriter); + public TweetService getTweetService(MetricWriter metricWriter, TweetRepository repository) { + return new TweetService(metricWriter, repository); } @Bean public TweetController getTweetConfiguration(TweetService tweetService) { return new TweetController(tweetService); } + } diff --git a/src/main/java/com/scmspain/controller/TweetController.java b/src/main/java/com/scmspain/controller/TweetController.java index 55ce7cd..8185d4a 100644 --- a/src/main/java/com/scmspain/controller/TweetController.java +++ b/src/main/java/com/scmspain/controller/TweetController.java @@ -1,20 +1,27 @@ package com.scmspain.controller; -import com.scmspain.controller.command.PublishTweetCommand; -import com.scmspain.entities.Tweet; -import com.scmspain.services.TweetService; -import org.springframework.web.bind.annotation.*; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.NO_CONTENT; import java.util.List; -import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.CREATED; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.scmspain.controller.command.DiscardTweetCommand; +import com.scmspain.controller.command.PublishTweetCommand; +import com.scmspain.entities.Tweet; +import com.scmspain.services.TweetService; @RestController public class TweetController { - private TweetService tweetService; + + private final TweetService tweetService; - public TweetController(TweetService tweetService) { + public TweetController(final TweetService tweetService) { this.tweetService = tweetService; } @@ -26,16 +33,19 @@ public List listAllTweets() { @PostMapping("/tweet") @ResponseStatus(CREATED) public void publishTweet(@RequestBody PublishTweetCommand publishTweetCommand) { - this.tweetService.publishTweet(publishTweetCommand.getPublisher(), publishTweetCommand.getTweet()); + this.tweetService.publishTweet(publishTweetCommand.getPublisher(), + publishTweetCommand.getTweet()); } - - @ExceptionHandler(IllegalArgumentException.class) - @ResponseStatus(BAD_REQUEST) - @ResponseBody - public Object invalidArgumentException(IllegalArgumentException ex) { - return new Object() { - public String message = ex.getMessage(); - public String exceptionClass = ex.getClass().getSimpleName(); - }; + + @GetMapping("/discarded") + public List listAllDiscardedTweets() { + return this.tweetService.listAllDiscardedTweets(); + } + + @PostMapping("/discarded") + @ResponseStatus(NO_CONTENT) + public void discardTweet(@RequestBody DiscardTweetCommand discardTweetCommand) { + this.tweetService.discardTweet(discardTweetCommand.getTweetId()); } + } diff --git a/src/main/java/com/scmspain/controller/command/DiscardTweetCommand.java b/src/main/java/com/scmspain/controller/command/DiscardTweetCommand.java new file mode 100644 index 0000000..6e36a0c --- /dev/null +++ b/src/main/java/com/scmspain/controller/command/DiscardTweetCommand.java @@ -0,0 +1,15 @@ +package com.scmspain.controller.command; + +public class DiscardTweetCommand { + + private Long tweetId; + + public Long getTweetId() { + return tweetId; + } + + public void setTweetId(Long tweetId) { + this.tweetId = tweetId; + } + +} diff --git a/src/main/java/com/scmspain/controller/command/PublishTweetCommand.java b/src/main/java/com/scmspain/controller/command/PublishTweetCommand.java index 543897b..fe0ea97 100644 --- a/src/main/java/com/scmspain/controller/command/PublishTweetCommand.java +++ b/src/main/java/com/scmspain/controller/command/PublishTweetCommand.java @@ -1,6 +1,7 @@ package com.scmspain.controller.command; public class PublishTweetCommand { + private String publisher; private String tweet; @@ -17,6 +18,7 @@ public String getTweet() { } public void setTweet(String tweet) { - this.tweet = tweet; + this.tweet = tweet; } + } diff --git a/src/main/java/com/scmspain/entities/Link.java b/src/main/java/com/scmspain/entities/Link.java new file mode 100644 index 0000000..68534a8 --- /dev/null +++ b/src/main/java/com/scmspain/entities/Link.java @@ -0,0 +1,57 @@ +package com.scmspain.entities; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +import com.scmspain.utils.TextElement; + +@Entity +public class Link implements TextElement { + + public static final String REGEX_PATTERN = "\\b(http|https)://\\S+\\b"; + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String value; + + @Column(nullable = false) + private int index; + + public Link() { + } + + public Link(String text, int index) { + super(); + this.value = text; + this.index = index; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public String getValue() { + return value; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String toString() { + return "Link [linkText=" + value + ", position=" + index + "]"; + } + +} \ No newline at end of file diff --git a/src/main/java/com/scmspain/entities/Tweet.java b/src/main/java/com/scmspain/entities/Tweet.java index 3616a94..d224b3a 100644 --- a/src/main/java/com/scmspain/entities/Tweet.java +++ b/src/main/java/com/scmspain/entities/Tweet.java @@ -1,26 +1,63 @@ package com.scmspain.entities; +import java.time.Instant; +import java.util.List; + +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.scmspain.utils.TextElementInserter; @Entity public class Tweet { + @Id @GeneratedValue private Long id; + @Column(nullable = false) + @NotNull + @Size(min=1) private String publisher; + @Column(nullable = false, length = 140) + @NotNull + @Size(min=1, max = 140) private String tweet; + @Column (nullable=true) private Long pre2015MigrationStatus = 0L; + + @Column (nullable=false) + private Instant publicationDateTime; + + @Column (nullable=true) + private Instant discardedDateTime; + + @Column (nullable=true) + @OneToMany(cascade = {CascadeType.ALL}) + private List links; public Tweet() { } - public Long getId() { + public Tweet(String tweet, String publisher, List links) { + super(); + this.tweet = tweet; + this.publisher = publisher; + this.links = links; + this.publicationDateTime = Instant.now(); + } + + public Long getId() { return id; } @@ -36,8 +73,9 @@ public void setPublisher(String publisher) { this.publisher = publisher; } + @JsonProperty("tweet") public String getTweet() { - return tweet; + return insertLinksOnTweet(tweet, links); } public void setTweet(String tweet) { @@ -52,4 +90,26 @@ public void setPre2015MigrationStatus(Long pre2015MigrationStatus) { this.pre2015MigrationStatus = pre2015MigrationStatus; } + @JsonIgnore + public Instant getDiscardedDateTime() { + return discardedDateTime; + } + + public void setDiscardedDateTime(Instant discardedDateTime) { + this.discardedDateTime = discardedDateTime; + } + + @JsonIgnore + public Instant getPublicationDateTime() { + return publicationDateTime; + } + + public void setPublicationDateTime(Instant publicationDateTime) { + this.publicationDateTime = publicationDateTime; + } + + private String insertLinksOnTweet(String text, List links) { + return new TextElementInserter().apply(text, links); + } + } diff --git a/src/main/java/com/scmspain/exceptions/CustomExceptionHandler.java b/src/main/java/com/scmspain/exceptions/CustomExceptionHandler.java new file mode 100644 index 0000000..82aa223 --- /dev/null +++ b/src/main/java/com/scmspain/exceptions/CustomExceptionHandler.java @@ -0,0 +1,52 @@ +package com.scmspain.exceptions; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.scmspain.controller.TweetController; +import com.scmspain.exceptions.ExceptionResponse; +import com.scmspain.exceptions.ResponseError; + +@EnableWebMvc +@ControllerAdvice(assignableTypes = TweetController.class) +public class CustomExceptionHandler { + + @ExceptionHandler(Exception.class) + @ResponseBody + public final ResponseEntity handleAllExceptions(Exception ex, WebRequest request) { + ExceptionResponse exceptionResponse = new ExceptionResponse(ex.getMessage()); + return new ResponseEntity<>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(ValidationException.class) + @ResponseBody + public final ResponseEntity invalidValidationException(ValidationException ex) { + if (ex instanceof ConstraintViolationException) { + List errors = ((ConstraintViolationException) ex).getConstraintViolations().stream() + .map(c -> new ResponseError("" + c.getPropertyPath(), c.getMessage())) + .collect(Collectors.toList()); + + + return new ResponseEntity<>(new ExceptionResponse("Invalid field(s)", errors), HttpStatus.BAD_REQUEST); + } + return new ResponseEntity<>(new ExceptionResponse(ex.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(TweetNotFoundException.class) + @ResponseBody + public final ResponseEntity invalidTweetNotFoundException(TweetNotFoundException ex) { + return new ResponseEntity<>(new ExceptionResponse(ex.getMessage()), HttpStatus.NOT_FOUND); + } + +} \ No newline at end of file diff --git a/src/main/java/com/scmspain/exceptions/ExceptionResponse.java b/src/main/java/com/scmspain/exceptions/ExceptionResponse.java new file mode 100644 index 0000000..f5847f0 --- /dev/null +++ b/src/main/java/com/scmspain/exceptions/ExceptionResponse.java @@ -0,0 +1,19 @@ +package com.scmspain.exceptions; + +import java.util.List; + +public class ExceptionResponse { + + public String message; + public List errors; + + public ExceptionResponse(String message) { + this.message = message; + } + + public ExceptionResponse(String message, List errors) { + this.message = message; + this.errors = errors; + } + +} \ No newline at end of file diff --git a/src/main/java/com/scmspain/exceptions/ResponseError.java b/src/main/java/com/scmspain/exceptions/ResponseError.java new file mode 100644 index 0000000..f0f7e04 --- /dev/null +++ b/src/main/java/com/scmspain/exceptions/ResponseError.java @@ -0,0 +1,24 @@ +package com.scmspain.exceptions; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +public class ResponseError { + + private String value; + private String message; + + public ResponseError(String value, String message) { + this.value = value; + this.message = message; + } + + public String getValue() { + return value; + } + + public String getMessage() { + return message; + } + +} \ No newline at end of file diff --git a/src/main/java/com/scmspain/exceptions/TweetNotFoundException.java b/src/main/java/com/scmspain/exceptions/TweetNotFoundException.java new file mode 100644 index 0000000..dab003c --- /dev/null +++ b/src/main/java/com/scmspain/exceptions/TweetNotFoundException.java @@ -0,0 +1,18 @@ +package com.scmspain.exceptions; + +public class TweetNotFoundException extends RuntimeException { + + private static final long serialVersionUID = 6283877308769877003L; + + private final Long tweet; + + public TweetNotFoundException(Long tweet) { + super("No Tweet published found with id " + tweet); + this.tweet = tweet; + } + + public Long getTweet() { + return tweet; + } + +} diff --git a/src/main/java/com/scmspain/repository/TweetRepository.java b/src/main/java/com/scmspain/repository/TweetRepository.java new file mode 100644 index 0000000..55910df --- /dev/null +++ b/src/main/java/com/scmspain/repository/TweetRepository.java @@ -0,0 +1,20 @@ +package com.scmspain.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import com.scmspain.entities.Tweet; + +@Repository +public interface TweetRepository extends CrudRepository { + + @Query("FROM Tweet WHERE pre2015MigrationStatus<>99 AND discardedDateTime=NULL ORDER BY publicationDateTime DESC") + List findAllSortedByPublicationDate(); + + @Query("FROM Tweet WHERE pre2015MigrationStatus<>99 AND discardedDateTime<>NULL ORDER BY discardedDateTime DESC") + List findAllDiscardedSortedByDiscardedDate(); + +} \ No newline at end of file diff --git a/src/main/java/com/scmspain/services/TweetService.java b/src/main/java/com/scmspain/services/TweetService.java index d61bc9d..b19a528 100644 --- a/src/main/java/com/scmspain/services/TweetService.java +++ b/src/main/java/com/scmspain/services/TweetService.java @@ -1,25 +1,43 @@ package com.scmspain.services; -import com.scmspain.entities.Tweet; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.transaction.Transactional; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + import org.springframework.boot.actuate.metrics.writer.Delta; import org.springframework.boot.actuate.metrics.writer.MetricWriter; import org.springframework.stereotype.Service; -import javax.persistence.EntityManager; -import javax.persistence.TypedQuery; -import javax.transaction.Transactional; -import java.util.ArrayList; -import java.util.List; +import com.scmspain.entities.Link; +import com.scmspain.entities.Tweet; +import com.scmspain.exceptions.TweetNotFoundException; +import com.scmspain.repository.TweetRepository; +import com.scmspain.utils.PatternExtractor; +import com.scmspain.utils.PatternExtractor.PatternExtractorResult; @Service @Transactional public class TweetService { - private EntityManager entityManager; - private MetricWriter metricWriter; - public TweetService(EntityManager entityManager, MetricWriter metricWriter) { - this.entityManager = entityManager; + private final MetricWriter metricWriter; + private final Validator validator; + private final TweetRepository tweetRepository; + + public TweetService(final MetricWriter metricWriter, + final TweetRepository repository) { this.metricWriter = metricWriter; + this.tweetRepository = repository; + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + this.validator = factory.getValidator(); } /** @@ -29,16 +47,21 @@ public TweetService(EntityManager entityManager, MetricWriter metricWriter) { Result - recovered Tweet */ public void publishTweet(String publisher, String text) { - if (publisher != null && publisher.length() > 0 && text != null && text.length() > 0 && text.length() < 140) { - Tweet tweet = new Tweet(); - tweet.setTweet(text); - tweet.setPublisher(publisher); + List links = new ArrayList<>(); + if (text != null) { + PatternExtractorResult patternExtractorResult = new PatternExtractor().apply(Link.REGEX_PATTERN, text); + links = patternExtractorResult.getExtractedPatternMap().entrySet().stream() + .map((e) -> new Link(e.getValue(), e.getKey())) + .collect(Collectors.toList()); + text = patternExtractorResult.getText(); + } + Tweet tweet = new Tweet(text, publisher, links); + Set> constraints = validator.validate(tweet); + if (constraints.size() != 0) + throw new ConstraintViolationException(constraints); - this.metricWriter.increment(new Delta("published-tweets", 1)); - this.entityManager.persist(tweet); - } else { - throw new IllegalArgumentException("Tweet must not be greater than 140 characters"); - } + this.metricWriter.increment(new Delta("published-tweets", 1)); + tweetRepository.save(tweet); } /** @@ -47,22 +70,40 @@ public void publishTweet(String publisher, String text) { Result - retrieved Tweet */ public Tweet getTweet(Long id) { - return this.entityManager.find(Tweet.class, id); + return tweetRepository.findOne(id); } /** - Recover tweet from repository - Parameter - id - id of the Tweet to retrieve - Result - retrieved Tweet + Recover tweets from repository + Result - retrieved lost of Tweet */ public List listAllTweets() { - List result = new ArrayList(); this.metricWriter.increment(new Delta("times-queried-tweets", 1)); - TypedQuery query = this.entityManager.createQuery("SELECT id FROM Tweet AS tweetId WHERE pre2015MigrationStatus<>99 ORDER BY id DESC", Long.class); - List ids = query.getResultList(); - for (Long id : ids) { - result.add(getTweet(id)); - } - return result; + return tweetRepository.findAllSortedByPublicationDate(); } -} + + /** + Discard tweet from repository + Parameter - id - id of the Tweet to discard + Result - discarded Tweet + */ + public void discardTweet(Long tweetId) { + Tweet tweet = getTweet(tweetId); + if (tweet == null || tweet.getDiscardedDateTime() != null) + throw new TweetNotFoundException(tweetId); + + tweet.setDiscardedDateTime(Instant.now()); + this.metricWriter.increment(new Delta("discarded-tweets", 1)); + tweetRepository.save(tweet); + } + + /** + Recover discarded tweets from repository + Result - retrieved list of discard Tweets + */ + public List listAllDiscardedTweets() { + this.metricWriter.increment(new Delta("times-queried-discarded-tweets", 1)); + return tweetRepository.findAllDiscardedSortedByDiscardedDate(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/scmspain/utils/PatternExtractor.java b/src/main/java/com/scmspain/utils/PatternExtractor.java new file mode 100644 index 0000000..b1858c0 --- /dev/null +++ b/src/main/java/com/scmspain/utils/PatternExtractor.java @@ -0,0 +1,51 @@ +package com.scmspain.utils; + +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.scmspain.utils.PatternExtractor.PatternExtractorResult; + +public class PatternExtractor implements BiFunction { + + @Override + public PatternExtractorResult apply(String pattern, String text) { + Map extractedPatternMap = new TreeMap<>(); + extractedPatternMap.clear(); + Pattern r = Pattern.compile(pattern); + Matcher m = r.matcher(text); + StringBuffer sb = new StringBuffer(text.length()); + while (m.find()) { + text = m.group(0); + int position = m.start(0); + extractedPatternMap.put(position, text); + m.appendReplacement(sb, ""); + } + m.appendTail(sb); + return new PatternExtractorResult(extractedPatternMap, sb.toString()); + } + + public final class PatternExtractorResult { + + private final Map extractedPatternMap; + private final String text; + + public PatternExtractorResult(final Map extractedPatternMap, final String text) { + this.extractedPatternMap = extractedPatternMap; + this.text = text; + } + + public Map getExtractedPatternMap() { + return Collections.unmodifiableMap(extractedPatternMap); + } + + public String getText() { + return text; + } + + } + +} diff --git a/src/main/java/com/scmspain/utils/TextElement.java b/src/main/java/com/scmspain/utils/TextElement.java new file mode 100644 index 0000000..f54b090 --- /dev/null +++ b/src/main/java/com/scmspain/utils/TextElement.java @@ -0,0 +1,9 @@ +package com.scmspain.utils; + +public interface TextElement { + + int getIndex(); + + String getValue(); + +} diff --git a/src/main/java/com/scmspain/utils/TextElementInserter.java b/src/main/java/com/scmspain/utils/TextElementInserter.java new file mode 100644 index 0000000..89f19ab --- /dev/null +++ b/src/main/java/com/scmspain/utils/TextElementInserter.java @@ -0,0 +1,42 @@ +package com.scmspain.utils; + +import java.util.List; +import java.util.function.BiFunction; + +public class TextElementInserter implements BiFunction, String> { + + @Override + public String apply(String text, List elements) { + if (elements.size() == 0) + return text; + + char[] letters = text.toCharArray(); + StringBuffer sb = new StringBuffer(); + int lCounter = 0; + int idxLinks = 0; + int i = 0; + TextElement element = (TextElement) elements.get(idxLinks); + String value = element.getValue(); + + while (i < letters.length + 1 && idxLinks < elements.size()) { + element = (TextElement) elements.get(idxLinks); + if (lCounter == element.getIndex()) { + value = element.getValue(); + sb.append(value); + lCounter += value.length(); + ++idxLinks; + } + else { + sb.append(letters[i++]); + ++lCounter; + } + } + + while (i < letters.length) + sb.append(letters[i++]); + + return sb.toString(); + + } + +} diff --git a/src/test/java/com/scmspain/controller/TweetControllerTest.java b/src/test/java/com/scmspain/controller/TweetControllerTest.java index 4368add..67b63ca 100644 --- a/src/test/java/com/scmspain/controller/TweetControllerTest.java +++ b/src/test/java/com/scmspain/controller/TweetControllerTest.java @@ -1,7 +1,14 @@ package com.scmspain.controller; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.scmspain.configuration.TestConfiguration; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +import java.util.List; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -14,22 +21,18 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.web.context.WebApplicationContext; -import java.util.List; - -import static java.lang.String.format; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scmspain.configuration.TestConfiguration; @RunWith(SpringRunner.class) @SpringBootTest(classes = TestConfiguration.class) public class TweetControllerTest { + @Autowired private WebApplicationContext context; + private MockMvc mockMvc; - + @Before public void setUp() { this.mockMvc = webAppContextSetup(this.context).build(); @@ -42,8 +45,14 @@ public void shouldReturn200WhenInsertingAValidTweet() throws Exception { } @Test - public void shouldReturn400WhenInsertingAnInvalidTweet() throws Exception { + public void shouldReturn200WhenInsertingAValidTweetWithLinks() throws Exception { mockMvc.perform(newTweet("Schibsted Spain", "We are Schibsted Spain (look at our home page http://www.schibsted.es/), we own Vibbo, InfoJobs, fotocasa, coches.net and milanuncios. Welcome!")) + .andExpect(status().is(201)); + } + + @Test + public void shouldReturn400WhenInsertingAnInvalidTweet() throws Exception { + mockMvc.perform(newTweet("Schibsted Spain", "We are Schibsted Spain (look at our home page http://www.schibsted.es/), we own Vibbo, InfoJobs, fotocasa, coches.net and milanuncios. Welcome! This is the schibsted test for the candidates interview.")) .andExpect(status().is(400)); } @@ -65,5 +74,35 @@ private MockHttpServletRequestBuilder newTweet(String publisher, String tweet) { .contentType(MediaType.APPLICATION_JSON_UTF8) .content(format("{\"publisher\": \"%s\", \"tweet\": \"%s\"}", publisher, tweet)); } + + private MockHttpServletRequestBuilder discardTweet(Long tweet) { + return post("/discarded") + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(format("{\"tweetId\": \"%s\"}", tweet)); + } + + @Test + public void shouldReturn404WhenDiscardingNotExistingTweet() throws Exception { + newTweet("Prospect", "Breaking the law"); + mockMvc.perform(discardTweet(2L)) + .andExpect(status().is(404)); + } + + @Test + public void shouldReturnAllDiscardedTweets() throws Exception { + mockMvc.perform(newTweet("Yo", "How are you?")) + .andExpect(status().is(201)); + mockMvc.perform(newTweet("Maria", "How are you?")) + .andExpect(status().is(201)); + mockMvc.perform(discardTweet(2L)) + .andExpect(status().is(204)); + + MvcResult getResult = mockMvc.perform(get("/discarded")) + .andExpect(status().is(200)) + .andReturn(); + + String content = getResult.getResponse().getContentAsString(); + assertThat(new ObjectMapper().readValue(content, List.class).size()).isEqualTo(1); + } -} +} \ No newline at end of file diff --git a/src/test/java/com/scmspain/services/TweetServiceTest.java b/src/test/java/com/scmspain/services/TweetServiceTest.java index ac88fe5..01c9384 100644 --- a/src/test/java/com/scmspain/services/TweetServiceTest.java +++ b/src/test/java/com/scmspain/services/TweetServiceTest.java @@ -1,38 +1,89 @@ package com.scmspain.services; -import com.scmspain.entities.Tweet; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import javax.validation.ValidationException; + import org.junit.Before; import org.junit.Test; import org.springframework.boot.actuate.metrics.writer.MetricWriter; -import javax.persistence.EntityManager; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import com.scmspain.entities.Link; +import com.scmspain.entities.Tweet; +import com.scmspain.exceptions.TweetNotFoundException; +import com.scmspain.repository.TweetRepository; public class TweetServiceTest { - private EntityManager entityManager; private MetricWriter metricWriter; private TweetService tweetService; + private TweetRepository repository; @Before public void setUp() throws Exception { - this.entityManager = mock(EntityManager.class); + this.repository = mock(TweetRepository.class); this.metricWriter = mock(MetricWriter.class); - this.tweetService = new TweetService(entityManager, metricWriter); + this.tweetService = new TweetService(metricWriter, repository); } @Test public void shouldInsertANewTweet() throws Exception { tweetService.publishTweet("Guybrush Threepwood", "I am Guybrush Threepwood, mighty pirate."); - verify(entityManager).persist(any(Tweet.class)); + verify(repository).save(any(Tweet.class)); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = ValidationException.class) public void shouldThrowAnExceptionWhenTweetLengthIsInvalid() throws Exception { tweetService.publishTweet("Pirate", "LeChuck? He's the guy that went to the Governor's for dinner and never wanted to leave. He fell for her in a big way, but she told him to drop dead. So he did. Then things really got ugly."); } + + @Test(expected = ValidationException.class) + public void shouldThrowAnExceptionWhenTweetTextIsEmpty() throws Exception { + tweetService.publishTweet("Pirate", ""); + } + + @Test(expected = ValidationException.class) + public void shouldThrowAnExceptionWhenTweetPubliserIsEmpty() throws Exception { + tweetService.publishTweet("", "I am Guybrush Threepwood, mighty pirate."); + } + + @Test(expected = ValidationException.class) + public void shouldThrowAnExceptionWhenNullPubliser() throws Exception { + tweetService.publishTweet(null, "I am Guybrush Threepwood, mighty pirate."); + } + + @Test(expected = ValidationException.class) + public void shouldThrowAnExceptionWhenNullTweet() throws Exception { + tweetService.publishTweet("Maria", null); + } + + @Test + public void shouldInsertANewTweetWithLinks() throws Exception { + List links = new ArrayList<>(); + links.add(new Link("http://foogle.co", 4)); + links.add(new Link("http://google.com", 10)); + tweetService.publishTweet("Guybrush Threepwood", "I am Guybrush Threepwood, mighty pirate."); + + verify(repository).save(any(Tweet.class)); + } + + @Test(expected = TweetNotFoundException.class) + public void shouldThrowAnExceptionWhenDiscardTweetNotFound() throws Exception { + tweetService.discardTweet(10L); + } + + @Test + public void shouldDiscardATweet() throws Exception { + when(repository.findOne(1L)).thenReturn(new Tweet()); + tweetService.discardTweet(1L); + verify(repository).save(any(Tweet.class)); + } + } diff --git a/src/test/java/com/scmspain/utils/PatternExtractorTest.java b/src/test/java/com/scmspain/utils/PatternExtractorTest.java new file mode 100644 index 0000000..1a77b1a --- /dev/null +++ b/src/test/java/com/scmspain/utils/PatternExtractorTest.java @@ -0,0 +1,44 @@ +package com.scmspain.utils; + +import org.junit.Test; + +import com.scmspain.utils.PatternExtractor.PatternExtractorResult; + +import static org.junit.Assert.assertEquals; + +public class PatternExtractorTest { + + @Test + public void shouldExtractPatternAtEnd() throws Exception { + String pattern = "\\b(http|https)://\\S+\\b"; + String text = "Hey http://foogle.co"; + String expectedText = "Hey "; + PatternExtractorResult patternExtractorResult = new PatternExtractor().apply(pattern, text); + int extractedPatterns = patternExtractorResult.getExtractedPatternMap().entrySet().size(); + assertEquals(1, extractedPatterns); + assertEquals(expectedText, patternExtractorResult.getText()); + } + + @Test + public void shouldExtractPatternAtBegin() throws Exception { + String pattern = "\\b(http|https)://\\S+\\b"; + String text = "http://foogle.co Hey How are u?"; + String expectedText = " Hey How are u?"; + PatternExtractorResult patternExtractorResult = new PatternExtractor().apply(pattern, text); + int extractedPatterns = patternExtractorResult.getExtractedPatternMap().entrySet().size(); + assertEquals(1, extractedPatterns); + assertEquals(expectedText, patternExtractorResult.getText()); + } + + @Test + public void shouldExtractPatternInMiddle() throws Exception { + String pattern = "\\b(http|https)://\\S+\\b"; + String text = "Hey http://foogle.co bla https://foogle.co bla bla "; + String expectedText = "Hey bla bla bla "; + PatternExtractorResult patternExtractorResult = new PatternExtractor().apply(pattern, text); + int extractedPatterns = patternExtractorResult.getExtractedPatternMap().entrySet().size(); + assertEquals(2, extractedPatterns); + assertEquals(expectedText, patternExtractorResult.getText()); + } + +} diff --git a/src/test/java/com/scmspain/utils/TextElementInserterTest.java b/src/test/java/com/scmspain/utils/TextElementInserterTest.java new file mode 100644 index 0000000..0611ce0 --- /dev/null +++ b/src/test/java/com/scmspain/utils/TextElementInserterTest.java @@ -0,0 +1,56 @@ +package com.scmspain.utils; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import com.scmspain.entities.Link; + +public class TextElementInserterTest { + + @Test + public void shouldInsertElementsAtEnd() throws Exception { + String text = "Hey "; + List links = new ArrayList<>(); + links.add(new Link("http://foogle.co", 4)); + String textResult = new TextElementInserter().apply(text, links); + String expectedText = "Hey http://foogle.co"; + assertEquals(expectedText, textResult); + } + + @Test + public void shouldInsertElementsAtBeginning() throws Exception { + String text = " Hey "; + List links = new ArrayList<>(); + links.add(new Link("http://foogle.co", 0)); + String textResult = new TextElementInserter().apply(text, links); + String expectedText = "http://foogle.co Hey "; + assertEquals(expectedText, textResult); + } + + @Test + public void shouldInsertElementsAtBeginningAndEnd() throws Exception { + String text = " Hey "; + List links = new ArrayList<>(); + links.add(new Link("http://foogle.co", 0)); + links.add(new Link("http://google.com", 21)); + String textResult = new TextElementInserter().apply(text, links); + String expectedText = "http://foogle.co Hey http://google.com"; + assertEquals(expectedText, textResult); + } + + @Test + public void shouldInsertElementsInTheMiddle() throws Exception { + String text = "Hey bla end"; + List links = new ArrayList<>(); + links.add(new Link("http://foogle.co", 4)); + links.add(new Link("http://google.com", 25)); + String textResult = new TextElementInserter().apply(text, links); + String expectedText = "Hey http://foogle.co bla http://google.com end"; + assertEquals(expectedText, textResult); + } + +}