From c5059aa8ffe7c411365c66253ea2012efa47f8f8 Mon Sep 17 00:00:00 2001 From: Mohamed Mohamed Date: Tue, 5 Sep 2023 18:51:57 -0400 Subject: [PATCH] GROUP-18 Added a Data Loader Job to automatically add active groups and auto-disband expired groups. --- build.gradle | 2 + config/pmd/codestyle.xml | 6 +- .../groupservice/GroupServiceApplication.java | 2 + .../group/demo/GroupDemoLoader.java | 69 +++++++ .../group/domain/groups/GroupRepository.java | 4 + .../group/domain/groups/GroupService.java | 32 ++- src/main/resources/application.yml | 11 ++ .../cucumber/steps/ActiveGroupsPolicy.java | 16 ++ .../demo/GroupDemoLoaderIntegrationTest.java | 186 ++++++++++++++++++ .../group/demo/GroupDemoLoaderTest.java | 78 ++++++++ .../domain/groups/GroupRepositoryTest.java | 23 ++- .../group/domain/groups/GroupServiceTest.java | 65 +++--- .../group/testutility/GroupTestUtility.java | 45 +++++ .../web/GroupControllerWebFluxTests.java | 15 +- src/test/resources/features/Groups.feature | 7 +- 15 files changed, 519 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/grouphq/groupservice/group/demo/GroupDemoLoader.java create mode 100644 src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderIntegrationTest.java create mode 100644 src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderTest.java create mode 100644 src/test/java/com/grouphq/groupservice/group/testutility/GroupTestUtility.java diff --git a/build.gradle b/build.gradle index f081eb1..9f83401 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,7 @@ dependencies { // implementation "org.springframework.cloud:spring-cloud-starter-config" implementation "org.springframework.boot:spring-boot-starter-security:3.1.3" + implementation 'com.github.javafaker:javafaker:1.0.2' implementation "io.sentry:sentry-spring-boot-starter-jakarta:6.28.0" runtimeOnly 'org.flywaydb:flyway-core' @@ -81,6 +82,7 @@ dependencies { testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:postgresql' testImplementation 'org.testcontainers:r2dbc' + testImplementation 'org.awaitility:awaitility:4.2.0' // Dependencies needed for Cucumber testImplementation(platform("org.junit:junit-bom:5.10.0")) diff --git a/config/pmd/codestyle.xml b/config/pmd/codestyle.xml index 2d155d3..c52070e 100644 --- a/config/pmd/codestyle.xml +++ b/config/pmd/codestyle.xml @@ -52,7 +52,11 @@ - + + + + + diff --git a/src/main/java/com/grouphq/groupservice/GroupServiceApplication.java b/src/main/java/com/grouphq/groupservice/GroupServiceApplication.java index 740c775..34f09f7 100644 --- a/src/main/java/com/grouphq/groupservice/GroupServiceApplication.java +++ b/src/main/java/com/grouphq/groupservice/GroupServiceApplication.java @@ -3,11 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.scheduling.annotation.EnableScheduling; /** * The entry point to the application setting up the Spring Context. */ @SpringBootApplication +@EnableScheduling @ConfigurationPropertiesScan public class GroupServiceApplication { diff --git a/src/main/java/com/grouphq/groupservice/group/demo/GroupDemoLoader.java b/src/main/java/com/grouphq/groupservice/group/demo/GroupDemoLoader.java new file mode 100644 index 0000000..f443139 --- /dev/null +++ b/src/main/java/com/grouphq/groupservice/group/demo/GroupDemoLoader.java @@ -0,0 +1,69 @@ +package com.grouphq.groupservice.group.demo; + +import com.grouphq.groupservice.group.domain.groups.Group; +import com.grouphq.groupservice.group.domain.groups.GroupRepository; +import com.grouphq.groupservice.group.domain.groups.GroupService; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * A Spring Job scheduler for periodically adding active groups + * and auto-disbanding expired groups. + */ +@Component +public class GroupDemoLoader { + + private boolean initialStateLoaded; + + private final int initialGroupSize; + + private final int periodicGroupAdditionCount; + + private final GroupRepository groupRepository; + + private final GroupService groupService; + + /** + * Gathers dependencies and values needed for demo loader. + */ + public GroupDemoLoader(GroupService groupService, + + GroupRepository groupRepository, + + @Value("${group.loader.initial-group-size}") + int initialGroupSize, + + @Value("${group.loader.periodic-group-addition-count}") + int periodicGroupAdditionCount + ) { + this.groupService = groupService; + this.groupRepository = groupRepository; + + this.initialGroupSize = initialGroupSize; + this.periodicGroupAdditionCount = periodicGroupAdditionCount; + } + + @Scheduled(initialDelayString = "${group.loader.initial-group-delay}", + fixedDelayString = "${group.loader.periodic-group-addition-interval}", + timeUnit = TimeUnit.SECONDS) + void loadGroups() { + final int groupsToAdd = initialStateLoaded + ? periodicGroupAdditionCount : initialGroupSize; + + for (int i = 0; i < groupsToAdd; i++) { + final Group group = groupService.generateGroup(); + assert groupRepository != null; + groupRepository.save(group).subscribe(); + } + initialStateLoaded = true; + } + + @Scheduled(initialDelayString = "${group.expiry-checker.initial-check-delay}", + fixedDelayString = "${group.expiry-checker.check-interval}", + timeUnit = TimeUnit.SECONDS) + void expireGroups() { + groupService.expireGroups().subscribe(); + } +} diff --git a/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupRepository.java b/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupRepository.java index c4aba81..09e59b4 100644 --- a/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupRepository.java +++ b/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupRepository.java @@ -1,8 +1,10 @@ package com.grouphq.groupservice.group.domain.groups; +import java.time.Instant; import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Interface to perform Reactive operations against the repository's "groups" table. @@ -16,4 +18,6 @@ public interface GroupRepository extends ReactiveCrudRepository { @Query("SELECT * FROM groups") Flux getAllGroups(); + @Query("UPDATE groups SET status = :status WHERE groups.created_date < :expiryDate") + Mono expireGroupsPastExpiryDate(Instant expiryDate, GroupStatus status); } diff --git a/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupService.java b/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupService.java index 674b09d..d97ea74 100644 --- a/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupService.java +++ b/src/main/java/com/grouphq/groupservice/group/domain/groups/GroupService.java @@ -1,7 +1,12 @@ package com.grouphq.groupservice.group.domain.groups; +import com.github.javafaker.Faker; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * A service performing the main business logic for the Group Service application. @@ -10,12 +15,37 @@ public class GroupService { private final GroupRepository groupRepository; + private final int expiryTime; - public GroupService(GroupRepository groupRepository) { + public GroupService(GroupRepository groupRepository, + @Value("${group.expiry-checker.time}") int expiryTime) { this.groupRepository = groupRepository; + this.expiryTime = expiryTime; } public Flux getGroups() { return groupRepository.findGroupsByStatus(GroupStatus.ACTIVE); } + + public Mono expireGroups() { + final Instant expiryDate = Instant.now().minus(expiryTime, ChronoUnit.SECONDS); + return groupRepository.expireGroupsPastExpiryDate(expiryDate, GroupStatus.AUTO_DISBANDED); + } + + /** + * Generates a random group that a user may have created. + */ + public Group generateGroup() { + final Faker faker = new Faker(); + + // Generate capacities and ensure maxCapacity has the higher number + int currentCapacity = faker.number().numberBetween(1, 249); + int maxCapacity = faker.number().numberBetween(2, 250); + final int temp = maxCapacity; + maxCapacity = Math.max(currentCapacity, maxCapacity); + currentCapacity = Math.min(currentCapacity, temp); + + return Group.of(faker.lorem().sentence(), faker.lorem().sentence(20), + maxCapacity, currentCapacity, GroupStatus.ACTIVE); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4992d02..cf3b6f9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,6 +10,17 @@ server: connection-timeout: 2s idle-timeout: 15s +group: + expiry-checker: + time: 1800 + initial-check-delay: 0 + check-interval: 300 + loader: + initial-group-delay: 0 + initial-group-size: 3 + periodic-group-addition-interval: 300 + periodic-group-addition-count: 1 + spring: application: name: group-service diff --git a/src/test/java/com/grouphq/groupservice/cucumber/steps/ActiveGroupsPolicy.java b/src/test/java/com/grouphq/groupservice/cucumber/steps/ActiveGroupsPolicy.java index ea98c75..da1ce28 100644 --- a/src/test/java/com/grouphq/groupservice/cucumber/steps/ActiveGroupsPolicy.java +++ b/src/test/java/com/grouphq/groupservice/cucumber/steps/ActiveGroupsPolicy.java @@ -18,6 +18,8 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.util.List; + @DataR2dbcTest @Import({DataConfig.class, SecurityConfig.class}) @Tag("AcceptanceTest") @@ -70,4 +72,18 @@ public void iShouldBeGivenAListOfActiveGroups() { "All groups received should be active"); }); } + + @Given("any time") + public void anyTime() { + // such as now + } + + @Then("I should be given a list of at least {int} active groups") + public void iShouldBeGivenAListOfAtLeastActiveGroups(int activeGroupsNeeded) { + final List groups = groupRepository.getAllGroups().collectList().block(); + + assertThat(groups) + .filteredOn(group -> group.status().equals(GroupStatus.ACTIVE)) + .hasSizeGreaterThanOrEqualTo(activeGroupsNeeded); + } } diff --git a/src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderIntegrationTest.java b/src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderIntegrationTest.java new file mode 100644 index 0000000..ee7596b --- /dev/null +++ b/src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderIntegrationTest.java @@ -0,0 +1,186 @@ +package com.grouphq.groupservice.group.demo; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.grouphq.groupservice.config.DataConfig; +import com.grouphq.groupservice.group.domain.groups.Group; +import com.grouphq.groupservice.group.domain.groups.GroupRepository; +import com.grouphq.groupservice.group.domain.groups.GroupService; +import com.grouphq.groupservice.group.domain.groups.GroupStatus; +import com.grouphq.groupservice.group.testutility.GroupTestUtility; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +/** + * Tests GroupDemoLoader's method logic across GroupService, GroupRepository and the Database. + * This does not check Spring's scheduling logic. + * Properties for Spring's scheduling are overridden to prevent it from running during this test. + */ +@SpringBootTest +@Import(DataConfig.class) +@TestPropertySource(properties = { + "group.loader.initial-group-size=3", + "group.loader.initial-group-delay=10000", + "group.loader.periodic-group-addition-interval=10000", + "group.expiry-checker.time=1800", + "group.expiry-checker.initial-check-delay=10000", + "group.expiry-checker.check-interval=10000" +}) +@Testcontainers +@Tag("IntegrationTest") +class GroupDemoLoaderIntegrationTest { + + @Container + static final PostgreSQLContainer POSTGRESQL_CONTAINER = + new PostgreSQLContainer<>(DockerImageName.parse("postgres:14.4")); + + @Autowired + GroupDemoLoader groupDemoLoader; + + @Autowired + GroupRepository groupRepository; + + @Autowired + GroupService groupService; + + @DynamicPropertySource + static void postgresqlProperties(DynamicPropertyRegistry registry) { + registry.add("spring.r2dbc.url", GroupDemoLoaderIntegrationTest::r2dbcUrl); + registry.add("spring.r2dbc.username", POSTGRESQL_CONTAINER::getUsername); + registry.add("spring.r2dbc.password", POSTGRESQL_CONTAINER::getPassword); + registry.add("spring.flyway.url", POSTGRESQL_CONTAINER::getJdbcUrl); + } + + private static String r2dbcUrl() { + return String.format("r2dbc:postgresql://%s:%s/%s", POSTGRESQL_CONTAINER.getHost(), + POSTGRESQL_CONTAINER.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), + POSTGRESQL_CONTAINER.getDatabaseName()); + } + + @BeforeEach + void timesJobShouldHaveRun() { + StepVerifier.create(groupRepository.deleteAll()) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + @DisplayName("Loads groups to database based on external properties") + void loadsGroups( + @Value("${group.loader.initial-group-size}") + int initialGroupSize, + + @Value("${group.loader.periodic-group-addition-count}") + int periodicGroupAdditionCount + ) { + groupDemoLoader.loadGroups(); + + StepVerifier.create(groupRepository.getAllGroups()) + .expectNextCount(initialGroupSize) + .expectComplete() + .verify(Duration.ofSeconds(1)); + + groupDemoLoader.loadGroups(); + + StepVerifier.create(groupRepository.getAllGroups()) + .expectNextCount(initialGroupSize + periodicGroupAdditionCount) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + @DisplayName("Expires groups with time older than allowed expiry time") + void expiresGroups() { + Group[] testGroups = new Group[3]; + + for (int i = 0; i < testGroups.length; i++) { + testGroups[i] = GroupTestUtility.generateFullGroupDetails(Instant.now()); + } + + final List groupsSaved = new ArrayList<>(); + + StepVerifier.create(groupRepository.saveAll(Flux.just(testGroups))) + .recordWith(() -> groupsSaved) + .expectNextCount(3) + .expectComplete() + .verify(Duration.ofSeconds(1)); + + Group[] groupsToExpire = new Group[3]; + for (int i = 0; i < groupsSaved.size(); i++) { + final Group group = groupsSaved.get(i); + groupsToExpire[i] = new Group( + group.id(), group.title(), group.description(), + group.maxGroupSize(), group.currentGroupSize(), group.status(), + group.lastActive(), Instant.ofEpochMilli(0), group.lastModifiedDate(), + group.createdBy(), group.lastModifiedBy(), group.version() + ); + } + + StepVerifier.create(groupRepository.saveAll(Flux.just(groupsToExpire))) + .expectNextCount(3) + .expectComplete() + .verify(Duration.ofSeconds(1)); + + groupDemoLoader.expireGroups(); + + final List groups = new ArrayList<>(); + + StepVerifier.create(groupRepository.getAllGroups()) + .recordWith(() -> groups) + .expectNextCount(3) + .expectComplete() + .verify(Duration.ofSeconds(1)); + + assertThat(groups) + .filteredOn(group -> group.status().equals(GroupStatus.AUTO_DISBANDED)) + .hasSize(groups.size()); + } + + @Test + @DisplayName("Does not expire groups with time younger than allowed expiry time") + void expirationStatusJob() { + Group[] testGroups = new Group[3]; + + for (int i = 0; i < testGroups.length; i++) { + testGroups[i] = GroupTestUtility.generateFullGroupDetails(Instant.now()); + } + + StepVerifier.create(groupRepository.saveAll(Flux.just(testGroups))) + .expectNextCount(3) + .expectComplete() + .verify(Duration.ofSeconds(1)); + + groupDemoLoader.expireGroups(); + + final List groups = new ArrayList<>(); + + StepVerifier.create(groupRepository.getAllGroups()) + .recordWith(() -> groups) + .expectNextCount(3) + .expectComplete() + .verify(Duration.ofSeconds(1)); + + assertThat(groups) + .filteredOn(group -> group.status().equals(GroupStatus.ACTIVE)) + .hasSize(groups.size()); + } +} diff --git a/src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderTest.java b/src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderTest.java new file mode 100644 index 0000000..e6cfd73 --- /dev/null +++ b/src/test/java/com/grouphq/groupservice/group/demo/GroupDemoLoaderTest.java @@ -0,0 +1,78 @@ +package com.grouphq.groupservice.group.demo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.grouphq.groupservice.group.domain.groups.Group; +import com.grouphq.groupservice.group.domain.groups.GroupRepository; +import com.grouphq.groupservice.group.domain.groups.GroupService; +import com.grouphq.groupservice.group.domain.groups.GroupStatus; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Mono; + +/** + * Tests GroupDemoLoader's method logic. + */ +@Tag("UnitTest") +@ExtendWith(MockitoExtension.class) +class GroupDemoLoaderTest { + + @Mock + private GroupService groupService; + + @Mock + private GroupRepository groupRepository; + + private GroupDemoLoader groupDemoLoader; + + boolean wasSubscribed; + + @BeforeEach + public void setUp() { + groupDemoLoader = new GroupDemoLoader(groupService, groupRepository, + 3, 5); + } + + @Test + @DisplayName("Generates group and saves a group to the database") + void loaderTest() { + final Group testGroup = Group.of("A", "B", 5, 1, GroupStatus.ACTIVE); + final Mono customMono = new Mono<>() { + @Override + public void subscribe(@NotNull CoreSubscriber actual) { + wasSubscribed = true; + Mono.just(testGroup) + .subscribe(actual); + } + }; + + given(groupService.generateGroup()).willReturn(testGroup); + given(groupRepository.save(any(Group.class))).willReturn(customMono); + + groupDemoLoader.loadGroups(); + + verify(groupService, times(3)).generateGroup(); + verify(groupRepository, times(3)).save(any(Group.class)); + assertThat(wasSubscribed).isTrue(); + } + + @Test + @DisplayName("Updates active groups older than expiry time to auto-disbanded status") + void expiryTest() { + given(groupService.expireGroups()).willReturn(Mono.just(0)); + groupDemoLoader.expireGroups(); + verify(groupService, times(1)).expireGroups(); + } + +} diff --git a/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupRepositoryTest.java b/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupRepositoryTest.java index 418e6ba..07592f2 100644 --- a/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupRepositoryTest.java +++ b/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupRepositoryTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.grouphq.groupservice.config.DataConfig; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.AfterEach; @@ -86,13 +87,12 @@ void deleteRepositoryData() { void retrievesOnlyActiveGroups() { final List groupsReturned = new ArrayList<>(); - StepVerifier.create(groupRepository.getAllGroups()) + StepVerifier.create(groupRepository.findGroupsByStatus(GroupStatus.ACTIVE)) .recordWith(() -> groupsReturned) - .expectNextCount(3) + .expectNextCount(2) .verifyComplete(); assertThat(groupsReturned) - .filteredOn(group -> group.status() == GroupStatus.ACTIVE) .hasSize(2); } @@ -115,4 +115,21 @@ void retrievesAllGroups() { .filteredOn(group -> group.status() == GroupStatus.AUTO_DISBANDED) .hasSize(1); } + + @Test + @DisplayName("Expires groups before expiry date") + void expiresGroupsBeforeExpiryDate() { + final List groupsReturned = new ArrayList<>(); + + groupRepository.expireGroupsPastExpiryDate(Instant.now(), GroupStatus.AUTO_DISBANDED) + .thenMany(groupRepository.findGroupsByStatus(GroupStatus.AUTO_DISBANDED)) + .as(StepVerifier::create) + .recordWith(() -> groupsReturned) + .expectNextCount(3) + .verifyComplete(); + + assertThat(groupsReturned) + .filteredOn(group -> group.status() == GroupStatus.AUTO_DISBANDED) + .hasSize(3); + } } diff --git a/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupServiceTest.java b/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupServiceTest.java index ef62c3f..5911b93 100644 --- a/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupServiceTest.java +++ b/src/test/java/com/grouphq/groupservice/group/domain/groups/GroupServiceTest.java @@ -1,18 +1,22 @@ package com.grouphq.groupservice.group.domain.groups; +import static com.grouphq.groupservice.group.testutility.GroupTestUtility.generateFullGroupDetails; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import java.time.Duration; import java.time.Instant; -import java.time.temporal.ChronoUnit; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @Tag("UnitTest") @@ -22,39 +26,52 @@ class GroupServiceTest { @Mock private GroupRepository groupRepository; - @InjectMocks private GroupService groupService; - private static Group[] testGroups; - - @BeforeAll - static void setUp() { - final String groupOwner = "system"; - - testGroups = new Group[] { - new Group(123_456L, "Example Title", "Example Description", 10, - 1, GroupStatus.ACTIVE, Instant.now(), - Instant.now().minus(20, ChronoUnit.MINUTES), - Instant.now().minus(5, ChronoUnit.MINUTES), - groupOwner, groupOwner, 3), - new Group(7890L, "Example Title", "Example Description", 5, - 2, GroupStatus.ACTIVE, Instant.now(), - Instant.now().minus(12, ChronoUnit.MINUTES), - Instant.now().minus(1, ChronoUnit.MINUTES), - groupOwner, groupOwner, 2) - }; + @BeforeEach + public void setUp() { + groupService = new GroupService(groupRepository, 30); } @Test @DisplayName("Gets (active) groups") void retrievesOnlyActiveGroups() { + final Group[] testGroups = { + generateFullGroupDetails(Instant.now()), + generateFullGroupDetails(Instant.now()) + }; + final Flux mockedGroups = Flux.just(testGroups); given(groupRepository.findGroupsByStatus(GroupStatus.ACTIVE)).willReturn(mockedGroups); final Flux retrievedGroups = groupService.getGroups(); + StepVerifier.create(retrievedGroups) - .expectNext(testGroups[0], testGroups[1]) - .verifyComplete(); + .expectNextMatches(group -> matchGroup(group, testGroups[0], 0)) + .expectNextMatches(group -> matchGroup(group, testGroups[1], 1)) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + @DisplayName("Sends expire request to repository interface") + void expireGroups() { + final Mono mono = groupService.expireGroups(); + assertThat(mono).isNull(); + verify(groupRepository) + .expireGroupsPastExpiryDate(any(Instant.class), any(GroupStatus.class)); + } + + private boolean matchGroup(Group actual, Group expected, int index) { + if (actual.equals(expected)) { + return true; + } else { + throw new AssertionError( + String.format( + "Test group %d should equal returned group\nExpected: %s\nActual: %s", + index, expected, actual) + ); + } } } diff --git a/src/test/java/com/grouphq/groupservice/group/testutility/GroupTestUtility.java b/src/test/java/com/grouphq/groupservice/group/testutility/GroupTestUtility.java new file mode 100644 index 0000000..8ac719c --- /dev/null +++ b/src/test/java/com/grouphq/groupservice/group/testutility/GroupTestUtility.java @@ -0,0 +1,45 @@ +package com.grouphq.groupservice.group.testutility; + +import com.github.javafaker.Faker; +import com.grouphq.groupservice.group.domain.groups.Group; +import com.grouphq.groupservice.group.domain.groups.GroupStatus; +import java.time.Instant; + +/** + * Utility class for common functionality needed by multiple tests. + */ +public final class GroupTestUtility { + + private GroupTestUtility() {} + + /** + * Generates a group that would be found in the database. + * + * @return A group object with all details + */ + public static Group generateFullGroupDetails(Instant createdAt) { + final Faker faker = new Faker(); + + // Generate capacities and ensure maxCapacity has the higher number + int currentCapacity = faker.number().numberBetween(1, 249); + int maxCapacity = faker.number().numberBetween(2, 250); + final int temp = maxCapacity; + maxCapacity = Math.max(currentCapacity, maxCapacity); + currentCapacity = Math.min(currentCapacity, temp); + + return new Group( + faker.number().randomNumber(10, true), + faker.lorem().sentence(), + faker.lorem().sentence(20), + maxCapacity, + currentCapacity, + GroupStatus.ACTIVE, + Instant.now(), + createdAt, + Instant.now(), + "system", + "system", + 0 + ); + } +} diff --git a/src/test/java/com/grouphq/groupservice/group/web/GroupControllerWebFluxTests.java b/src/test/java/com/grouphq/groupservice/group/web/GroupControllerWebFluxTests.java index 2db6ca4..8b8eb46 100644 --- a/src/test/java/com/grouphq/groupservice/group/web/GroupControllerWebFluxTests.java +++ b/src/test/java/com/grouphq/groupservice/group/web/GroupControllerWebFluxTests.java @@ -7,8 +7,8 @@ import com.grouphq.groupservice.group.domain.groups.Group; import com.grouphq.groupservice.group.domain.groups.GroupService; import com.grouphq.groupservice.group.domain.groups.GroupStatus; +import com.grouphq.groupservice.group.testutility.GroupTestUtility; import java.time.Instant; -import java.time.temporal.ChronoUnit; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -33,18 +33,9 @@ class GroupControllerWebFluxTests { @Test @DisplayName("When there are active groups, then return a list of active groups") void returnActiveGroups() { - final String groupOwner = "system"; final Group[] testGroups = { - new Group(123_456L, "Example Title", "Example Description", 10, - 1, GroupStatus.ACTIVE, Instant.now(), - Instant.now().minus(20, ChronoUnit.MINUTES), - Instant.now().minus(5, ChronoUnit.MINUTES), - groupOwner, groupOwner, 3), - new Group(7890L, "Example Title", "Example Description", 5, - 2, GroupStatus.ACTIVE, Instant.now(), - Instant.now().minus(12, ChronoUnit.MINUTES), - Instant.now().minus(1, ChronoUnit.MINUTES), - groupOwner, groupOwner, 2) + GroupTestUtility.generateFullGroupDetails(Instant.now()), + GroupTestUtility.generateFullGroupDetails(Instant.now()) }; given(groupService.getGroups()).willReturn(Flux.just(testGroups)); diff --git a/src/test/resources/features/Groups.feature b/src/test/resources/features/Groups.feature index 8062f1f..ad1d7d7 100644 --- a/src/test/resources/features/Groups.feature +++ b/src/test/resources/features/Groups.feature @@ -4,4 +4,9 @@ Feature: Groups Scenario: There exists active groups Given there are active groups When I request groups - Then I should be given a list of active groups \ No newline at end of file + Then I should be given a list of active groups + + Scenario: Always have active groups + Given any time + When I request groups + Then I should be given a list of at least 3 active groups