From 4b542dcc2eaaee11681eceba7e1549f804e5876d Mon Sep 17 00:00:00 2001 From: Jordan Sim-Smith Date: Thu, 14 Nov 2024 13:58:52 +1300 Subject: [PATCH] Implement SUB Football registration page handler --- .../pricetracker/PriceTrackerTestModule.java | 2 +- subfootball_tracker_api/BUILD.bazel | 95 +++++++++++ .../JsoupSubfootballClient.java | 26 ++++ .../subfootballtracker/SubfootballClient.java | 5 + .../SubfootballTrackerFactory.java | 32 ++++ .../SubfootballTrackerItem.java | 147 ++++++++++++++++++ .../SubfootballTrackerModule.java | 25 +++ .../UpdatePageContentHandler.java | 86 ++++++++++ .../src/main/resources/logback.xml | 11 ++ .../FakeSubfootballClient.java | 18 +++ .../SubfootballTrackerTestFactory.java | 41 +++++ .../SubfootballTrackerTestModule.java | 31 ++++ ...datePageContentHandlerIntegrationTest.java | 82 ++++++++++ 13 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 subfootball_tracker_api/BUILD.bazel create mode 100644 subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/JsoupSubfootballClient.java create mode 100644 subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballClient.java create mode 100644 subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerFactory.java create mode 100644 subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerItem.java create mode 100644 subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerModule.java create mode 100644 subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandler.java create mode 100644 subfootball_tracker_api/src/main/resources/logback.xml create mode 100644 subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/FakeSubfootballClient.java create mode 100644 subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestFactory.java create mode 100644 subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestModule.java create mode 100644 subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandlerIntegrationTest.java diff --git a/price_tracker_api/src/test/java/com/jordansimsmith/pricetracker/PriceTrackerTestModule.java b/price_tracker_api/src/test/java/com/jordansimsmith/pricetracker/PriceTrackerTestModule.java index d8b2e76..0b9b9fe 100644 --- a/price_tracker_api/src/test/java/com/jordansimsmith/pricetracker/PriceTrackerTestModule.java +++ b/price_tracker_api/src/test/java/com/jordansimsmith/pricetracker/PriceTrackerTestModule.java @@ -11,7 +11,7 @@ public class PriceTrackerTestModule { @Provides @Singleton - public DynamoDbTable immersionTrackerTable( + public DynamoDbTable priceTrackerTable( DynamoDbEnhancedClient dynamoDbEnhancedClient) { var schema = TableSchema.fromBean(PriceTrackerItem.class); return dynamoDbEnhancedClient.table("price_tracker", schema); diff --git a/subfootball_tracker_api/BUILD.bazel b/subfootball_tracker_api/BUILD.bazel new file mode 100644 index 0000000..879a504 --- /dev/null +++ b/subfootball_tracker_api/BUILD.bazel @@ -0,0 +1,95 @@ +load("@contrib_rules_jvm//java:defs.bzl", "JUNIT5_DEPS", "java_test_suite") +load("@dagger//:workspace_defs.bzl", "dagger_rules") + +dagger_rules() + +HANDLERS = glob(["src/main/java/**/*Handler.java"]) + +INTEGRATION_TESTS = glob(["src/test/java/**/*IntegrationTest.java"]) + +java_library( + name = "lib", + srcs = glob( + ["src/main/java/**/*.java"], + exclude = HANDLERS, + ), + deps = [ + ":dagger", + "//lib/dynamodb:lib", + "//lib/notifications:lib", + "//lib/time:lib", + "@maven//:com_google_guava_guava", + "@maven//:org_jsoup_jsoup", + "@maven//:software_amazon_awssdk_dynamodb", + "@maven//:software_amazon_awssdk_dynamodb_enhanced", + ], +) + +java_binary( + name = "update-page-content-handler", + srcs = glob(["src/main/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandler.java"]), + create_executable = False, + resources = [ + "src/main/resources/logback.xml", + ], + deps = [ + ":lib", + "//lib/notifications:lib", + "//lib/time:lib", + "@maven//:ch_qos_logback_logback_classic", + "@maven//:ch_qos_logback_logback_core", + "@maven//:com_amazonaws_aws_lambda_java_core", + "@maven//:com_amazonaws_aws_lambda_java_events", + "@maven//:com_google_code_findbugs_jsr305", + "@maven//:com_google_guava_guava", + "@maven//:software_amazon_awssdk_dynamodb_enhanced", + ], +) + +java_library( + name = "test-lib", + srcs = glob( + ["src/test/java/**/*.java"], + exclude = INTEGRATION_TESTS, + ), + deps = [ + ":dagger", + ":lib", + "//lib/dynamodb:lib", + "//lib/notifications:lib", + "//lib/time:lib", + "@maven//:com_google_guava_guava", + "@maven//:software_amazon_awssdk_dynamodb", + "@maven//:software_amazon_awssdk_dynamodb_enhanced", + ], +) + +java_test_suite( + name = "integration-tests", + size = "medium", + srcs = INTEGRATION_TESTS, + env = { + "AWS_ACCESS_KEY_ID": "fake", + "AWS_SECRET_ACCESS_KEY": "fake", + "AWS_REGION": "ap-southeast-2", + }, + runner = "junit5", + test_suffixes = ["IntegrationTest.java"], + runtime_deps = JUNIT5_DEPS, + deps = [ + ":lib", + ":test-lib", + ":update-page-content-handler", + "//lib/dynamodb:lib", + "//lib/notifications:lib", + "//lib/testcontainers:lib", + "//lib/time:lib", + "@maven//:com_amazonaws_aws_lambda_java_core", + "@maven//:com_amazonaws_aws_lambda_java_events", + "@maven//:org_assertj_assertj_core", + "@maven//:org_junit_jupiter_junit_jupiter_api", + "@maven//:org_testcontainers_junit_jupiter", + "@maven//:software_amazon_awssdk_dynamodb", + "@maven//:software_amazon_awssdk_dynamodb_enhanced", + ], +) diff --git a/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/JsoupSubfootballClient.java b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/JsoupSubfootballClient.java new file mode 100644 index 0000000..3565ffe --- /dev/null +++ b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/JsoupSubfootballClient.java @@ -0,0 +1,26 @@ +package com.jordansimsmith.subfootballtracker; + +import com.google.common.base.Preconditions; +import org.jsoup.Jsoup; + +public class JsoupSubfootballClient implements SubfootballClient { + @Override + public String getRegistrationContent() { + try { + return doGetRegistrationContent(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String doGetRegistrationContent() throws Exception { + var doc = Jsoup.connect("https://subfootball.com/register").get(); + + var content = doc.selectFirst(".page.content-item"); + Preconditions.checkNotNull(content); + + content.select("br").before("\\n"); + content.select("p").before("\\n"); + return content.text().replaceAll(" *\\\\n *", "\n").trim(); + } +} diff --git a/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballClient.java b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballClient.java new file mode 100644 index 0000000..f754c26 --- /dev/null +++ b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballClient.java @@ -0,0 +1,5 @@ +package com.jordansimsmith.subfootballtracker; + +public interface SubfootballClient { + String getRegistrationContent(); +} diff --git a/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerFactory.java b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerFactory.java new file mode 100644 index 0000000..6543650 --- /dev/null +++ b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerFactory.java @@ -0,0 +1,32 @@ +package com.jordansimsmith.subfootballtracker; + +import com.jordansimsmith.dynamodb.DynamoDbModule; +import com.jordansimsmith.lib.notifications.NotificationModule; +import com.jordansimsmith.lib.notifications.NotificationPublisher; +import com.jordansimsmith.time.Clock; +import com.jordansimsmith.time.ClockModule; +import dagger.Component; +import javax.inject.Singleton; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; + +@Singleton +@Component( + modules = { + ClockModule.class, + NotificationModule.class, + DynamoDbModule.class, + SubfootballTrackerModule.class + }) +public interface SubfootballTrackerFactory { + Clock clock(); + + NotificationPublisher notificationPublisher(); + + DynamoDbTable subfootballTrackerTable(); + + SubfootballClient subfootballClient(); + + static SubfootballTrackerFactory create() { + return DaggerSubfootballTrackerFactory.create(); + } +} diff --git a/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerItem.java b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerItem.java new file mode 100644 index 0000000..a9087cc --- /dev/null +++ b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerItem.java @@ -0,0 +1,147 @@ +package com.jordansimsmith.subfootballtracker; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class SubfootballTrackerItem { + public enum Page { + REGISTRATION + } + + public static final String DELIMITER = "#"; + public static final String PAGE_PREFIX = "PAGE" + DELIMITER; + public static final String TIMESTAMP_PREFIX = "TIMESTAMP" + DELIMITER; + + private static final String PK = "pk"; + private static final String SK = "sk"; + private static final String PAGE = "page"; + private static final String TIMESTAMP = "timestamp"; + private static final String CONTENT = "content"; + private static final String VERSION = "version"; + + private String pk; + private String sk; + private Page page; + private Long timestamp; + private String content; + private Long version; + + @DynamoDbPartitionKey + @DynamoDbAttribute(PK) + public String getPk() { + return pk; + } + + public void setPk(String pk) { + this.pk = pk; + } + + @DynamoDbSortKey + @DynamoDbAttribute(SK) + public String getSk() { + return sk; + } + + public void setSk(String sk) { + this.sk = sk; + } + + @DynamoDbAttribute(PAGE) + public Page getPage() { + return page; + } + + public void setPage(Page page) { + this.page = page; + } + + @DynamoDbAttribute(TIMESTAMP) + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + @DynamoDbAttribute(CONTENT) + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + @DynamoDbVersionAttribute + @DynamoDbAttribute(VERSION) + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public String toString() { + return "SubfootballTrackerItem{" + + "pk='" + + pk + + '\'' + + ", sk='" + + sk + + '\'' + + ", page='" + + page + + '\'' + + ", timestamp=" + + timestamp + + ", content='" + + content + + '\'' + + ", version=" + + version + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SubfootballTrackerItem that = (SubfootballTrackerItem) o; + return Objects.equals(pk, that.pk) + && Objects.equals(sk, that.sk) + && Objects.equals(page, that.page) + && Objects.equals(timestamp, that.timestamp) + && Objects.equals(content, that.content) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(pk, sk, page, timestamp, content, version); + } + + public static String formatPk(Page page) { + return PAGE_PREFIX + page; + } + + public static String formatSk(long timestamp) { + return TIMESTAMP_PREFIX + timestamp; + } + + public static SubfootballTrackerItem create(Page page, long timestamp, String content) { + var subfootballTrackerItem = new SubfootballTrackerItem(); + subfootballTrackerItem.setPk(formatPk(page)); + subfootballTrackerItem.setSk(formatSk(timestamp)); + subfootballTrackerItem.setContent(content); + subfootballTrackerItem.setTimestamp(timestamp); + subfootballTrackerItem.setPage(page); + return subfootballTrackerItem; + } +} diff --git a/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerModule.java b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerModule.java new file mode 100644 index 0000000..53fb4a5 --- /dev/null +++ b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerModule.java @@ -0,0 +1,25 @@ +package com.jordansimsmith.subfootballtracker; + +import dagger.Module; +import dagger.Provides; +import javax.inject.Singleton; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; + +@Module +public class SubfootballTrackerModule { + @Provides + @Singleton + public DynamoDbTable subfootballTrackerTable( + DynamoDbEnhancedClient dynamoDbEnhancedClient) { + var schema = TableSchema.fromBean(SubfootballTrackerItem.class); + return dynamoDbEnhancedClient.table("subfootball_tracker", schema); + } + + @Provides + @Singleton + public SubfootballClient subfootballClient() { + return new JsoupSubfootballClient(); + } +} diff --git a/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandler.java b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandler.java new file mode 100644 index 0000000..bb3f2fb --- /dev/null +++ b/subfootball_tracker_api/src/main/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandler.java @@ -0,0 +1,86 @@ +package com.jordansimsmith.subfootballtracker; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import com.google.common.annotations.VisibleForTesting; +import com.jordansimsmith.lib.notifications.NotificationPublisher; +import com.jordansimsmith.time.Clock; +import java.util.StringJoiner; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; + +public class UpdatePageContentHandler implements RequestHandler { + @VisibleForTesting static final String TOPIC = "subfootball_tracker_api_page_content_updates"; + + private final Clock clock; + private final NotificationPublisher notificationPublisher; + private final DynamoDbTable subfootballTrackerTable; + private final SubfootballClient subfootballClient; + + public UpdatePageContentHandler() { + this(SubfootballTrackerFactory.create()); + } + + @VisibleForTesting + UpdatePageContentHandler(SubfootballTrackerFactory factory) { + this.clock = factory.clock(); + this.notificationPublisher = factory.notificationPublisher(); + this.subfootballTrackerTable = factory.subfootballTrackerTable(); + this.subfootballClient = factory.subfootballClient(); + } + + @Override + public Void handleRequest(ScheduledEvent event, Context context) { + try { + return doHandleRequest(event, context); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Void doHandleRequest(ScheduledEvent event, Context context) throws Exception { + var now = clock.now(); + + var content = subfootballClient.getRegistrationContent(); + var previous = + subfootballTrackerTable + .query( + QueryEnhancedRequest.builder() + .queryConditional( + QueryConditional.keyEqualTo( + Key.builder() + .partitionValue( + SubfootballTrackerItem.formatPk( + SubfootballTrackerItem.Page.REGISTRATION)) + .build())) + .limit(1) + .scanIndexForward(false) + .build()) + .items() + .stream() + .findFirst() + .orElse(null); + + if (previous != null && !previous.getContent().equals(content)) { + var subject = "SUB Football registration page updated"; + var message = new StringJoiner("\r\n\r\n"); + message.add("The latest content reads:"); + var lines = content.split("\\r?\\n"); + for (var line : lines) { + message.add(line); + } + + notificationPublisher.publish(TOPIC, subject, message.toString()); + } + + var current = + SubfootballTrackerItem.create( + SubfootballTrackerItem.Page.REGISTRATION, now.getEpochSecond(), content); + subfootballTrackerTable.putItem(current); + + return null; + } +} diff --git a/subfootball_tracker_api/src/main/resources/logback.xml b/subfootball_tracker_api/src/main/resources/logback.xml new file mode 100644 index 0000000..43e1dbe --- /dev/null +++ b/subfootball_tracker_api/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + \ No newline at end of file diff --git a/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/FakeSubfootballClient.java b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/FakeSubfootballClient.java new file mode 100644 index 0000000..a44a60a --- /dev/null +++ b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/FakeSubfootballClient.java @@ -0,0 +1,18 @@ +package com.jordansimsmith.subfootballtracker; + +public class FakeSubfootballClient implements SubfootballClient { + private String registrationContent; + + @Override + public String getRegistrationContent() { + return registrationContent; + } + + public void setRegistrationContent(String registrationContent) { + this.registrationContent = registrationContent; + } + + public void reset() { + registrationContent = null; + } +} diff --git a/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestFactory.java b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestFactory.java new file mode 100644 index 0000000..b7859d5 --- /dev/null +++ b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestFactory.java @@ -0,0 +1,41 @@ +package com.jordansimsmith.subfootballtracker; + +import com.jordansimsmith.dynamodb.DynamoDbTestModule; +import com.jordansimsmith.lib.notifications.FakeNotificationPublisher; +import com.jordansimsmith.lib.notifications.NotificationTestModule; +import com.jordansimsmith.time.ClockTestModule; +import com.jordansimsmith.time.FakeClock; +import dagger.BindsInstance; +import dagger.Component; +import java.net.URI; +import javax.inject.Named; +import javax.inject.Singleton; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +@Singleton +@Component( + modules = { + ClockTestModule.class, + DynamoDbTestModule.class, + NotificationTestModule.class, + SubfootballTrackerTestModule.class + }) +public interface SubfootballTrackerTestFactory extends SubfootballTrackerFactory { + FakeClock fakeClock(); + + DynamoDbClient dynamoDbClient(); + + FakeNotificationPublisher fakeNotificationPublisher(); + + FakeSubfootballClient fakeSubfootballClient(); + + @Component.Factory + interface Factory { + SubfootballTrackerTestFactory create( + @BindsInstance @Named("dynamoDbEndpoint") URI dynamoDbEndpoint); + } + + static SubfootballTrackerTestFactory create(URI dynamoDbEndpoint) { + return DaggerSubfootballTrackerTestFactory.factory().create(dynamoDbEndpoint); + } +} diff --git a/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestModule.java b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestModule.java new file mode 100644 index 0000000..0788535 --- /dev/null +++ b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/SubfootballTrackerTestModule.java @@ -0,0 +1,31 @@ +package com.jordansimsmith.subfootballtracker; + +import dagger.Module; +import dagger.Provides; +import javax.inject.Singleton; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; + +@Module +public class SubfootballTrackerTestModule { + @Provides + @Singleton + public DynamoDbTable subfootballTrackerTable( + DynamoDbEnhancedClient dynamoDbEnhancedClient) { + var schema = TableSchema.fromBean(SubfootballTrackerItem.class); + return dynamoDbEnhancedClient.table("subfootball_tracker", schema); + } + + @Provides + @Singleton + public FakeSubfootballClient fakeSubfootballClient() { + return new FakeSubfootballClient(); + } + + @Provides + @Singleton + public SubfootballClient subfootballClient(FakeSubfootballClient fakeSubfootballClient) { + return fakeSubfootballClient; + } +} diff --git a/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandlerIntegrationTest.java b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandlerIntegrationTest.java new file mode 100644 index 0000000..4590220 --- /dev/null +++ b/subfootball_tracker_api/src/test/java/com/jordansimsmith/subfootballtracker/UpdatePageContentHandlerIntegrationTest.java @@ -0,0 +1,82 @@ +package com.jordansimsmith.subfootballtracker; + +import static org.assertj.core.api.Assertions.*; + +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import com.jordansimsmith.dynamodb.DynamoDbUtils; +import com.jordansimsmith.lib.notifications.FakeNotificationPublisher; +import com.jordansimsmith.testcontainers.DynamoDbContainer; +import com.jordansimsmith.time.FakeClock; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; + +@Testcontainers +public class UpdatePageContentHandlerIntegrationTest { + private FakeClock fakeClock; + private FakeNotificationPublisher fakeNotificationPublisher; + private FakeSubfootballClient fakeSubfootballClient; + private DynamoDbTable subfootballTrackerTable; + + private UpdatePageContentHandler updatePageContentHandler; + + @Container DynamoDbContainer dynamoDbContainer = new DynamoDbContainer(); + + @BeforeEach + void setUp() { + var factory = SubfootballTrackerTestFactory.create(dynamoDbContainer.getEndpoint()); + + fakeClock = factory.fakeClock(); + fakeNotificationPublisher = factory.fakeNotificationPublisher(); + fakeSubfootballClient = factory.fakeSubfootballClient(); + subfootballTrackerTable = factory.subfootballTrackerTable(); + + DynamoDbUtils.createTable(factory.dynamoDbClient(), subfootballTrackerTable); + + updatePageContentHandler = new UpdatePageContentHandler(factory); + } + + @Test + void handleRequestShouldUpdatePageContent() { + // arrange + var contentHistory1 = + SubfootballTrackerItem.create( + SubfootballTrackerItem.Page.REGISTRATION, 1_000, "content 1\ncontent 1"); + var contentHistory2 = + SubfootballTrackerItem.create( + SubfootballTrackerItem.Page.REGISTRATION, 2_000, "content 2\ncontent 2"); + subfootballTrackerTable.putItem(contentHistory1); + subfootballTrackerTable.putItem(contentHistory2); + + fakeClock.setTime(3_000); + + fakeSubfootballClient.setRegistrationContent("content 3\ncontent 3"); + + // act + updatePageContentHandler.handleRequest(new ScheduledEvent(), null); + + // assert + var contentHistory3 = + subfootballTrackerTable.getItem( + Key.builder() + .partitionValue( + SubfootballTrackerItem.formatPk(SubfootballTrackerItem.Page.REGISTRATION)) + .sortValue(SubfootballTrackerItem.formatSk(fakeClock.now().getEpochSecond())) + .build()); + assertThat(contentHistory3).isNotNull(); + assertThat(contentHistory3.getPage()).isEqualTo(SubfootballTrackerItem.Page.REGISTRATION); + assertThat(contentHistory3.getTimestamp()).isEqualTo(fakeClock.now().getEpochSecond()); + assertThat(contentHistory3.getContent()) + .isEqualTo(fakeSubfootballClient.getRegistrationContent()); + + var notifications = fakeNotificationPublisher.findNotifications(UpdatePageContentHandler.TOPIC); + assertThat(notifications.size()).isEqualTo(1); + var notification = notifications.get(0); + assertThat(notification.subject()).isEqualTo("SUB Football registration page updated"); + assertThat(notification.message()) + .isEqualTo("The latest content reads:\r\n\r\ncontent 3\r\n\r\ncontent 3"); + } +}