diff --git a/src/mooc/build.gradle b/src/mooc/build.gradle index 986f161..16fe6b5 100644 --- a/src/mooc/build.gradle +++ b/src/mooc/build.gradle @@ -1,3 +1,11 @@ dependencies { compile project(":src:shared") + + compileOnly 'org.projectlombok:lombok:1.18.16' + annotationProcessor 'org.projectlombok:lombok:1.18.16' + + testCompileOnly 'org.projectlombok:lombok:1.18.16' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.16' + + compile 'org.twitter4j:twitter4j-stream:4.0.6' } diff --git a/src/mooc/main/tv/codely/mooc/errors/application/ProcessErrorGenerated.java b/src/mooc/main/tv/codely/mooc/errors/application/ProcessErrorGenerated.java new file mode 100644 index 0000000..cfc913d --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/errors/application/ProcessErrorGenerated.java @@ -0,0 +1,22 @@ +package tv.codely.mooc.errors.application; + +import lombok.AllArgsConstructor; +import tv.codely.mooc.errors.domain.ErrorGenerated; +import tv.codely.mooc.errors.domain.ErrorProcessor; +import tv.codely.shared.application.DomainEventSubscriber; + +@AllArgsConstructor +public class ProcessErrorGenerated implements DomainEventSubscriber { + + ErrorProcessor errorProcessor; + + @Override + public Class subscribedTo() { + return ErrorGenerated.class; + } + + @Override + public void consume(ErrorGenerated event) { + errorProcessor.processError(event); + } +} diff --git a/src/mooc/main/tv/codely/mooc/errors/domain/ErrorGenerated.java b/src/mooc/main/tv/codely/mooc/errors/domain/ErrorGenerated.java new file mode 100644 index 0000000..554e3c1 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/errors/domain/ErrorGenerated.java @@ -0,0 +1,18 @@ +package tv.codely.mooc.errors.domain; + +import lombok.Value; +import tv.codely.shared.domain.DomainEvent; + +@Value +public class ErrorGenerated implements DomainEvent { + + String errorMessage; + Throwable cause; + + private static final String FULL_QUALIFIED_EVENT_NAME = "codelytv.errors.error.generated"; + + @Override + public String fullQualifiedEventName() { + return FULL_QUALIFIED_EVENT_NAME; + } +} diff --git a/src/mooc/main/tv/codely/mooc/errors/domain/ErrorProcessor.java b/src/mooc/main/tv/codely/mooc/errors/domain/ErrorProcessor.java new file mode 100644 index 0000000..6f20f6f --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/errors/domain/ErrorProcessor.java @@ -0,0 +1,7 @@ +package tv.codely.mooc.errors.domain; + +public interface ErrorProcessor { + + void processError (ErrorGenerated error); + +} diff --git a/src/mooc/main/tv/codely/mooc/errors/infrastructure/ErrorProcessorSystemErr.java b/src/mooc/main/tv/codely/mooc/errors/infrastructure/ErrorProcessorSystemErr.java new file mode 100644 index 0000000..1af47d5 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/errors/infrastructure/ErrorProcessorSystemErr.java @@ -0,0 +1,11 @@ +package tv.codely.mooc.errors.infrastructure; + +import tv.codely.mooc.errors.domain.ErrorGenerated; +import tv.codely.mooc.errors.domain.ErrorProcessor; + +public class ErrorProcessorSystemErr implements ErrorProcessor { + @Override + public void processError(ErrorGenerated error) { + System.err.println(error.getErrorMessage() + ":" + error.getCause()); + } +} diff --git a/src/mooc/main/tv/codely/mooc/notification/application/create/SendTweetOnVideoPublished.java b/src/mooc/main/tv/codely/mooc/notification/application/create/SendTweetOnVideoPublished.java new file mode 100644 index 0000000..dfc71dd --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/notification/application/create/SendTweetOnVideoPublished.java @@ -0,0 +1,32 @@ +package tv.codely.mooc.notification.application.create; + +import lombok.AllArgsConstructor; +import tv.codely.mooc.errors.domain.ErrorGenerated; +import tv.codely.mooc.notification.domain.TwitterException; +import tv.codely.mooc.notification.domain.TwitterPublisher; +import tv.codely.mooc.video.domain.VideoPublished; +import tv.codely.shared.application.DomainEventSubscriber; +import tv.codely.shared.domain.EventBus; + +import static java.util.Arrays.asList; + +@AllArgsConstructor +public class SendTweetOnVideoPublished implements DomainEventSubscriber { + + TwitterPublisher publisher; + EventBus eventBus; + + @Override + public Class subscribedTo() { + return VideoPublished.class; + } + + @Override + public void consume(VideoPublished event) { + try { + publisher.tweet("New video published: " + event.title()); + } catch (TwitterException e) { + eventBus.publish(asList(new ErrorGenerated("Error tweeting after video published", e))); + } + } +} diff --git a/src/mooc/main/tv/codely/mooc/notification/domain/TwitterException.java b/src/mooc/main/tv/codely/mooc/notification/domain/TwitterException.java new file mode 100644 index 0000000..843dad4 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/notification/domain/TwitterException.java @@ -0,0 +1,8 @@ +package tv.codely.mooc.notification.domain; + +public class TwitterException extends Exception{ + + public TwitterException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/mooc/main/tv/codely/mooc/notification/domain/TwitterPublisher.java b/src/mooc/main/tv/codely/mooc/notification/domain/TwitterPublisher.java new file mode 100644 index 0000000..b058064 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/notification/domain/TwitterPublisher.java @@ -0,0 +1,6 @@ +package tv.codely.mooc.notification.domain; + +public interface TwitterPublisher { + + void tweet (String text) throws TwitterException; +} diff --git a/src/mooc/main/tv/codely/mooc/notification/infrastructure/FakeTwitterPublisher.java b/src/mooc/main/tv/codely/mooc/notification/infrastructure/FakeTwitterPublisher.java new file mode 100644 index 0000000..6ce9662 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/notification/infrastructure/FakeTwitterPublisher.java @@ -0,0 +1,10 @@ +package tv.codely.mooc.notification.infrastructure; + +import tv.codely.mooc.notification.domain.TwitterPublisher; + +public class FakeTwitterPublisher implements TwitterPublisher { + @Override + public void tweet(String text) { + //Do nothing here + } +} diff --git a/src/mooc/main/tv/codely/mooc/notification/infrastructure/Twitter4JPublisher.java b/src/mooc/main/tv/codely/mooc/notification/infrastructure/Twitter4JPublisher.java new file mode 100644 index 0000000..7b3588d --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/notification/infrastructure/Twitter4JPublisher.java @@ -0,0 +1,40 @@ +package tv.codely.mooc.notification.infrastructure; + +import lombok.AllArgsConstructor; +import tv.codely.mooc.notification.domain.TwitterPublisher; +import twitter4j.Twitter; +import twitter4j.TwitterException; +import twitter4j.TwitterFactory; +import twitter4j.conf.ConfigurationBuilder; + +@AllArgsConstructor +public class Twitter4JPublisher implements TwitterPublisher { + + String consumerKey; + String consumerSecret; + String accessToken; + String accessTokenSecret; + + protected Twitter getTwitterInstance() { + ConfigurationBuilder cb = new ConfigurationBuilder(); + cb.setOAuthConsumerKey(consumerKey) + .setOAuthConsumerSecret(consumerSecret) + .setOAuthAccessToken(accessToken) + .setOAuthAccessTokenSecret(accessTokenSecret); + TwitterFactory tf = new TwitterFactory(cb.build()); + return tf.getInstance(); + } + + @Override + public void tweet(String text) throws tv.codely.mooc.notification.domain.TwitterException { + tweet(text, getTwitterInstance()); + } + + private void tweet(String text, Twitter twitterInstance) throws tv.codely.mooc.notification.domain.TwitterException { + try { + twitterInstance.updateStatus(text); + } catch (TwitterException e) { + throw new tv.codely.mooc.notification.domain.TwitterException("Error updating status " + text, e); + } + } +} diff --git a/src/mooc/main/tv/codely/mooc/video/infrastructure/VideoPublisherCliController.java b/src/mooc/main/tv/codely/mooc/video/infrastructure/VideoPublisherCliController.java index 864eb8e..820923e 100644 --- a/src/mooc/main/tv/codely/mooc/video/infrastructure/VideoPublisherCliController.java +++ b/src/mooc/main/tv/codely/mooc/video/infrastructure/VideoPublisherCliController.java @@ -1,19 +1,28 @@ package tv.codely.mooc.video.infrastructure; +import tv.codely.mooc.errors.application.ProcessErrorGenerated; +import tv.codely.mooc.errors.domain.ErrorProcessor; +import tv.codely.mooc.errors.infrastructure.ErrorProcessorSystemErr; import tv.codely.mooc.notification.application.create.SendPushToSubscribersOnVideoPublished; +import tv.codely.mooc.notification.application.create.SendTweetOnVideoPublished; +import tv.codely.mooc.notification.domain.TwitterPublisher; +import tv.codely.mooc.notification.infrastructure.FakeTwitterPublisher; import tv.codely.mooc.video.application.publish.VideoPublisher; import tv.codely.shared.application.DomainEventSubscriber; -import tv.codely.shared.domain.EventBus; import tv.codely.shared.infrastructure.bus.ReactorEventBus; import java.util.Set; public class VideoPublisherCliController { public static void main(String[] args) { + final TwitterPublisher twitterPublisher = new FakeTwitterPublisher(); + final ErrorProcessor errorProcessor = new ErrorProcessorSystemErr(); final Set subscribers = Set.of( - new SendPushToSubscribersOnVideoPublished() + new SendPushToSubscribersOnVideoPublished(), + new ProcessErrorGenerated(errorProcessor) ); - final EventBus eventBus = new ReactorEventBus(subscribers); + final ReactorEventBus eventBus = new ReactorEventBus(subscribers); + eventBus.addSubscriber(new SendTweetOnVideoPublished(twitterPublisher, eventBus)); final var videoPublisher = new VideoPublisher(eventBus); final var videoTitle = "\uD83C\uDF89 New YouTube.com/CodelyTV video title"; diff --git a/src/mooc/test/tv/codely/mooc/errors/application/ProcessErrorGeneratedTest.java b/src/mooc/test/tv/codely/mooc/errors/application/ProcessErrorGeneratedTest.java new file mode 100644 index 0000000..7f29f62 --- /dev/null +++ b/src/mooc/test/tv/codely/mooc/errors/application/ProcessErrorGeneratedTest.java @@ -0,0 +1,32 @@ +package tv.codely.mooc.errors.application; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.errors.domain.ErrorGenerated; +import tv.codely.mooc.errors.domain.ErrorProcessor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class ProcessErrorGeneratedTest { + + ProcessErrorGenerated processErrorGenerated; + + ErrorProcessor errorProcessor; + + @BeforeEach + void setUp() { + errorProcessor = mock(ErrorProcessor.class); + processErrorGenerated = new ProcessErrorGenerated(errorProcessor); + } + + @Test + void consume() { + ErrorGenerated errorEvent = new ErrorGenerated("error", new IllegalArgumentException("")); + + processErrorGenerated.consume(errorEvent); + + verify(errorProcessor).processError(errorEvent); + } +} \ No newline at end of file diff --git a/src/mooc/test/tv/codely/mooc/notification/application/create/SendTweetOnVideoPublishedShould.java b/src/mooc/test/tv/codely/mooc/notification/application/create/SendTweetOnVideoPublishedShould.java new file mode 100644 index 0000000..af1c5a5 --- /dev/null +++ b/src/mooc/test/tv/codely/mooc/notification/application/create/SendTweetOnVideoPublishedShould.java @@ -0,0 +1,47 @@ +package tv.codely.mooc.notification.application.create; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.errors.domain.ErrorGenerated; +import tv.codely.mooc.notification.domain.TwitterException; +import tv.codely.mooc.notification.domain.TwitterPublisher; +import tv.codely.mooc.video.domain.VideoPublished; +import tv.codely.shared.domain.EventBus; + +import static java.util.Arrays.asList; +import static org.mockito.Mockito.*; + +class SendTweetOnVideoPublishedShould { + + SendTweetOnVideoPublished sendTweetOnVideoPublished; + + TwitterPublisher publisher; + EventBus eventBus; + + @BeforeEach + void setUp() { + publisher = mock(TwitterPublisher.class); + eventBus = mock(EventBus.class); + sendTweetOnVideoPublished = new SendTweetOnVideoPublished(publisher, eventBus); + } + + @Test + void consume_should_send_text_to_twitter() throws TwitterException { + VideoPublished videoPublished = new VideoPublished("title", "description"); + + sendTweetOnVideoPublished.consume(videoPublished); + + verify(publisher).tweet("New video published: " + videoPublished.title()); + } + + @Test + void consume_should_send_notify_error_if_exception_tweeting() throws TwitterException { + VideoPublished videoPublished = new VideoPublished("title", "description"); + TwitterException exception = new TwitterException("error", new IllegalArgumentException("")); + doThrow(exception).when(publisher).tweet(anyString()); + + sendTweetOnVideoPublished.consume(videoPublished); + + verify(eventBus).publish(asList(new ErrorGenerated("Error tweeting after video published", exception))); + } +} \ No newline at end of file diff --git a/src/mooc/test/tv/codely/mooc/notification/infrastructure/Twitter4JPublisherShould.java b/src/mooc/test/tv/codely/mooc/notification/infrastructure/Twitter4JPublisherShould.java new file mode 100644 index 0000000..437a045 --- /dev/null +++ b/src/mooc/test/tv/codely/mooc/notification/infrastructure/Twitter4JPublisherShould.java @@ -0,0 +1,43 @@ +package tv.codely.mooc.notification.infrastructure; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import twitter4j.Twitter; +import twitter4j.TwitterException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class Twitter4JPublisherShould { + + Twitter4JPublisher publisher; + + Twitter twitterInstance; + + @BeforeEach + void setUp() { + twitterInstance = mock(Twitter.class); + publisher = spy(new Twitter4JPublisher("consumerKey", "consumerSecret", "accessToken", "accessTokenSecret")); + doReturn(twitterInstance).when(publisher).getTwitterInstance(); + } + + @Test + void post_tweet() throws tv.codely.mooc.notification.domain.TwitterException, TwitterException { + String text = "Text to tweet"; + + publisher.tweet(text); + + verify(twitterInstance).updateStatus(text); + } + + @Test + void post_tweet_with_error() throws TwitterException { + String text = "Text to tweet"; + doThrow(new TwitterException("Error")).when(twitterInstance).updateStatus(text); + + assertThrows(tv.codely.mooc.notification.domain.TwitterException.class, () -> { + publisher.tweet(text); + }); + } + +} \ No newline at end of file diff --git a/src/shared/main/tv.codely.shared/infrastructure/bus/ReactorEventBus.java b/src/shared/main/tv.codely.shared/infrastructure/bus/ReactorEventBus.java index 13bfad9..4dcf571 100644 --- a/src/shared/main/tv.codely.shared/infrastructure/bus/ReactorEventBus.java +++ b/src/shared/main/tv.codely.shared/infrastructure/bus/ReactorEventBus.java @@ -21,6 +21,10 @@ public ReactorEventBus(final Set subscribers) { subscribers.forEach(this::registerOnEventBus); } + public void addSubscriber (DomainEventSubscriber subscriber) { + registerOnEventBus(subscriber); + } + @Override public void publish(final List events) { events.forEach(this::publish);