From f0b508ccb5294919e4006cbebba7c4b6fbeaa18a Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Wed, 3 Jan 2024 10:30:29 +0100 Subject: [PATCH 1/6] chore(habit): add spring modulith as dependency --- services/habit/build.gradle | 12 ++++++++ ...gelog-1.5-add-event-publication-table.yaml | 30 +++++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 4 ++- .../codecentric/hc/habit/auth/AuthTest.java | 4 ++- 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 services/habit/src/main/resources/db/changelog/db.changelog-1.5-add-event-publication-table.yaml diff --git a/services/habit/build.gradle b/services/habit/build.gradle index 03a1fc70..dd1a123d 100644 --- a/services/habit/build.gradle +++ b/services/habit/build.gradle @@ -83,7 +83,19 @@ ext { ext['junit-jupiter.version'] = versions.junitJupiter ext['jna.version'] = versions.jna // Required for Docker on ARM +dependencyManagement { + imports { + mavenBom 'org.springframework.modulith:spring-modulith-bom:1.1.0' + } +} + dependencies { + // Spring Modulith + implementation "org.springframework.modulith:spring-modulith-starter-core" + implementation "org.springframework.modulith:spring-modulith-starter-jpa" + implementation "org.springframework.modulith:spring-modulith-events-kafka" + intTestImplementation "org.springframework.modulith:spring-modulith-starter-test" + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/services/habit/src/main/resources/db/changelog/db.changelog-1.5-add-event-publication-table.yaml b/services/habit/src/main/resources/db/changelog/db.changelog-1.5-add-event-publication-table.yaml new file mode 100644 index 00000000..e94094a9 --- /dev/null +++ b/services/habit/src/main/resources/db/changelog/db.changelog-1.5-add-event-publication-table.yaml @@ -0,0 +1,30 @@ +databaseChangeLog: + - changeSet: + id: add-event-publication-table + author: dennis.effing@codecentric.de + changes: + - createTable: + tableName: event_publication + columns: + - column: + name: id + type: UUID + - column: + name: listener_id + type: TEXT + - column: + name: event_type + type: TEXT + - column: + name: serialized_event + type: TEXT + - column: + name: publication_date + type: TIMESTAMP + - column: + name: completion_date + type: TIMESTAMP + - addPrimaryKey: + tableName: event_publication + constraintName: event_publication_pkey + columnNames: id diff --git a/services/habit/src/main/resources/db/changelog/db.changelog-master.yaml b/services/habit/src/main/resources/db/changelog/db.changelog-master.yaml index 9251ad3b..6d96c563 100644 --- a/services/habit/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/services/habit/src/main/resources/db/changelog/db.changelog-master.yaml @@ -10,4 +10,6 @@ databaseChangeLog: - include: file: db/changelog/db.changelog-1.4-add-fixed-unique-habit-name-constraint.yaml - include: - file: db/changelog/db.changelog-strip-classpath-changelog-prefix.yaml \ No newline at end of file + file: db/changelog/db.changelog-1.5-add-event-publication-table.yaml + - include: + file: db/changelog/db.changelog-strip-classpath-changelog-prefix.yaml diff --git a/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java b/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java index 90fee516..912db203 100644 --- a/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java +++ b/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java @@ -32,7 +32,9 @@ import org.springframework.test.web.servlet.MockMvc; @AutoConfigureMockMvc -@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) +@EnableAutoConfiguration( + exclude = DataSourceAutoConfiguration.class, + excludeName = "org.springframework.modulith.events.jpa.JpaEventPublicationAutoConfiguration") @ExtendWith(SpringExtension.class) @SpringBootTest public class AuthTest { From 05008e2e63b2585f51a7dcfaa642b7ba511e041b Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Wed, 3 Jan 2024 10:57:27 +0100 Subject: [PATCH 2/6] feat(habit): publish habit created event when habit is created --- .../habits/HabitModuleIntegrationTest.java | 42 +++++++++++++++++++ .../de/codecentric/hc/habit/habits/Habit.java | 28 +++++++++---- 2 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java diff --git a/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java new file mode 100644 index 00000000..a4009f32 --- /dev/null +++ b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java @@ -0,0 +1,42 @@ +package de.codecentric.hc.habit.habits; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.codecentric.hc.habit.auth.UserIdArgumentResolver; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.modulith.test.ApplicationModuleTest; +import org.springframework.modulith.test.Scenario; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("intTest") +@ApplicationModuleTest +@MockBean(UserIdArgumentResolver.class) +public class HabitModuleIntegrationTest { + + @Autowired private HabitController habitController; + + @Test + void shouldPublishHabitCreatedEventWhenHabitIsCreated(Scenario scenario) { + Habit.ModificationRequest createJoggingHabitRequest = + Habit.ModificationRequest.builder() + .name("Jogging") + .schedule( + Habit.Schedule.builder() + .frequency(Habit.Schedule.Frequency.WEEKLY) + .repetitions(3) + .build()) + .build(); + + scenario + .stimulate(() -> habitController.createHabit(createJoggingHabitRequest, "userId")) + .andWaitForEventOfType(Habit.HabitCreated.class) + .toArriveAndVerify( + event -> { + assertThat(event.name()).isEqualTo("Jogging"); + assertThat(event.frequency()).isEqualTo(Habit.Schedule.Frequency.WEEKLY); + assertThat(event.repetitions()).isEqualTo(3); + }); + } +} diff --git a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java index a77d5608..da98d886 100644 --- a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java +++ b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import org.springframework.data.domain.AbstractAggregateRoot; @Entity @Builder @@ -29,12 +30,12 @@ @NoArgsConstructor @Setter @Getter -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = false) @ToString @Table( uniqueConstraints = @UniqueConstraint(columnNames = "name", name = Habit.CONSTRAINT_NAME_UNIQUE_NAME)) -public class Habit { +public class Habit extends AbstractAggregateRoot { public static final String CONSTRAINT_NAME_UNIQUE_NAME = "unique_habit_name"; @@ -77,11 +78,17 @@ public enum Frequency { } public static Habit from(ModificationRequest modificationRequest, String userId) { - return builder() - .name(modificationRequest.getName()) - .schedule(modificationRequest.getSchedule()) - .userId(userId) - .build(); + Habit habit = + builder() + .name(modificationRequest.getName()) + .schedule(modificationRequest.getSchedule()) + .userId(userId) + .build(); + habit.registerEvent( + new Habit.HabitCreated( + habit.id, habit.name, habit.schedule.frequency, habit.schedule.repetitions)); + + return habit; } /** @@ -103,4 +110,11 @@ public static class ModificationRequest { @NotNull @Valid private Schedule schedule; } + + public record HabitCreated( + Long habitId, String name, Schedule.Frequency frequency, Integer repetitions) { + public String getId() { + return habitId.toString(); + } + } } From 7da732b9ec4bfb6f8e2608bcc7e96fdcc92ff380 Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Wed, 3 Jan 2024 13:12:12 +0100 Subject: [PATCH 3/6] chore: use uuids instead of longs for habit ids This fixes the habit ID being null in the HabitCreated event. Since the habit ID was generated by Hibernate using a database sequence, the ID was not yet populated when the HabitCreated event is published. --- services/habit/pacts/hc-ui-hc-habit.json | 15 +- .../habit/habits/HabitControllerIntTest.java | 14 +- .../habits/HabitControllerJwtIntTest.java | 11 +- .../verification/UiPactVerificationTest.java | 20 +- .../hc/habit/testing/CustomMatchers.java | 34 ++ .../de/codecentric/hc/habit/habits/Habit.java | 12 +- .../hc/habit/habits/HabitController.java | 5 +- .../hc/habit/habits/HabitRepository.java | 5 +- .../db.changelog-1.0-create-schema.yaml | 4 +- .../codecentric/hc/habit/auth/AuthTest.java | 3 +- .../hc/habit/habits/HabitControllerTest.java | 18 +- .../kotlin/de/codecentric/hc/report/Habit.kt | 6 +- .../hc/report/HabitTrackingService.kt | 13 +- .../codecentric/hc/report/HabitServiceTest.kt | 37 +- .../hc/report/HabitTrackingServiceTest.kt | 40 +- .../hc/report/TrackedHabitServiceTest.kt | 75 ++-- .../codecentric/hc/report/TrackedHabitTest.kt | 370 +++++++++--------- .../habit/HabitModuleIntegrationTest.java | 19 +- ...abitTrackingControllerRestAssuredTest.java | 17 +- ...tTrackingControllerJwtRestAssuredTest.java | 3 +- .../track/habit/matcher/HabitApiMatcher.java | 2 +- .../habit/matcher/HabitApiMatcherTest.java | 4 +- .../track/habit/HabitTracking.java | 12 +- .../track/habit/HabitTrackingController.java | 27 +- .../track/habit/HabitTrackingRepository.java | 3 +- .../track/habit/validation/HabitId.java | 24 -- .../db/migration/V1__create-schema.sql | 19 +- .../HabitTrackingControllerWebMvcTest.java | 3 +- .../track/habit/HabitTrackingTest.java | 23 +- .../HabitTrackingControllerJwtWebMvcTest.java | 2 +- services/ui/src/overview/api/habit/api.ts | 8 +- services/ui/src/overview/api/habit/habit.ts | 4 +- services/ui/src/overview/api/track/api.ts | 8 +- .../src/overview/api/track/useTrackedDates.ts | 4 +- .../overview/components/list/HabitItem.tsx | 6 +- .../components/list/TrackDatePicker.tsx | 11 +- 36 files changed, 471 insertions(+), 410 deletions(-) create mode 100644 services/habit/src/intTest/java/de/codecentric/hc/habit/testing/CustomMatchers.java delete mode 100644 services/track/src/main/java/de/codecentric/habitcentric/track/habit/validation/HabitId.java diff --git a/services/habit/pacts/hc-ui-hc-habit.json b/services/habit/pacts/hc-ui-hc-habit.json index ee070a45..7370b900 100644 --- a/services/habit/pacts/hc-ui-hc-habit.json +++ b/services/habit/pacts/hc-ui-hc-habit.json @@ -23,7 +23,6 @@ }, "body": [ { - "id": 1, "name": "Jogging", "schedule": { "repetitions": 2, @@ -31,7 +30,6 @@ } }, { - "id": 101, "name": "Meditate", "schedule": { "repetitions": 1, @@ -39,7 +37,6 @@ } }, { - "id": 51, "name": "Play guitar", "schedule": { "repetitions": 5, @@ -49,13 +46,13 @@ ], "matchingRules": { "$.body[0].id": { - "match": "type" + "match": "uuid" }, "$.body[1].id": { - "match": "type" + "match": "uuid" }, "$.body[2].id": { - "match": "type" + "match": "uuid" } } } @@ -84,10 +81,10 @@ }, { "description": "request to delete the habit with id '123'", - "providerState": "habit with id '123' exists", + "providerState": "habit with id 'd712645f-cd4f-40c4-b171-bb2ea72d180d' exists", "request": { "method": "DELETE", - "path": "/habits/123" + "path": "/habits/d712645f-cd4f-40c4-b171-bb2ea72d180d" }, "response": { "status": 200, @@ -101,4 +98,4 @@ "version": "2.0.0" } } -} \ No newline at end of file +} diff --git a/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerIntTest.java b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerIntTest.java index 60fe0a5c..498ea21d 100644 --- a/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerIntTest.java +++ b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerIntTest.java @@ -2,6 +2,7 @@ import static de.codecentric.hc.habit.habits.Habit.Schedule.Frequency.DAILY; import static de.codecentric.hc.habit.habits.Habit.Schedule.Frequency.WEEKLY; +import static de.codecentric.hc.habit.testing.CustomMatchers.isValidUuid; import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; import static io.restassured.http.ContentType.JSON; @@ -9,7 +10,6 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.everyItem; -import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.isEmptyOrNullString; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CREATED; @@ -23,6 +23,7 @@ import de.codecentric.hc.habit.testing.RestAssuredTest; import io.restassured.http.Header; import java.sql.SQLException; +import java.util.UUID; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; @@ -61,7 +62,7 @@ public void getHabits() throws InterruptedException { .then() .statusCode(200) .body("name", contains(expected)) - .body("id", everyItem(greaterThan(0))); + .body("id", everyItem(isValidUuid())); } @Test @@ -79,7 +80,7 @@ public void getHabitsOrderedByNameAscending() throws InterruptedException { .then() .statusCode(200) .body("name", contains(expected)) - .body("id", everyItem(greaterThan(0))); + .body("id", everyItem(isValidUuid())); } @Test @@ -246,19 +247,20 @@ public void deleteHabit() { @Test public void deleteHabitNotFound() { + UUID habitId = UUID.randomUUID(); given() .header(DEFAULT_USER_ID_HEADER) .when() - .delete("/habits/{id}", 999) + .delete("/habits/{id}", habitId) .then() .statusCode(NOT_FOUND.value()) - .body("message", equalTo("Habit '999' could not be found.")); + .body("message", equalTo(String.format("Habit '%s' could not be found.", habitId))); } @Test public void deleteHabitWithoutAuthShouldFail() { when() - .delete("/habits/{id}", 123) + .delete("/habits/{id}", UUID.randomUUID()) .then() .statusCode( INTERNAL_SERVER_ERROR.value()) // TODO: HTTP 400 or 401 would be more appropriate diff --git a/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerJwtIntTest.java b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerJwtIntTest.java index 6bf9ca31..7f46a570 100644 --- a/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerJwtIntTest.java +++ b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitControllerJwtIntTest.java @@ -2,6 +2,7 @@ import static de.codecentric.hc.habit.habits.Habit.Schedule.Frequency.DAILY; import static de.codecentric.hc.habit.habits.Habit.Schedule.Frequency.WEEKLY; +import static de.codecentric.hc.habit.testing.CustomMatchers.isValidUuid; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; import static org.assertj.core.api.Assertions.assertThat; @@ -13,6 +14,7 @@ import de.codecentric.hc.habit.testing.RestAssuredTest; import io.restassured.http.Header; import java.sql.SQLException; +import java.util.UUID; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; @@ -57,7 +59,7 @@ public void getHabits() throws InterruptedException { .then() .statusCode(OK.value()) .body("name", contains(expected)) - .body("id", everyItem(greaterThan(0))); + .body("id", everyItem(isValidUuid())); } @Test @@ -75,7 +77,7 @@ public void getHabitsOrderedByNameAscending() throws InterruptedException { .then() .statusCode(OK.value()) .body("name", contains(expected)) - .body("id", everyItem(greaterThan(0))); + .body("id", everyItem(isValidUuid())); } @Test @@ -238,13 +240,14 @@ public void deleteHabit() { @Test public void deleteHabitNotFound() { + UUID habitId = UUID.randomUUID(); given() .header(DEFAULT_AUTHORIZATION_HEADER) .when() - .delete("/habits/{id}", 999) + .delete("/habits/{id}", habitId) .then() .statusCode(NOT_FOUND.value()) - .body("message", equalTo("Habit '999' could not be found.")); + .body("message", equalTo(String.format("Habit '%s' could not be found.", habitId))); } private String insertHabit(String name) { diff --git a/services/habit/src/intTest/java/de/codecentric/hc/habit/provider/verification/UiPactVerificationTest.java b/services/habit/src/intTest/java/de/codecentric/hc/habit/provider/verification/UiPactVerificationTest.java index b8bb7197..6670369e 100644 --- a/services/habit/src/intTest/java/de/codecentric/hc/habit/provider/verification/UiPactVerificationTest.java +++ b/services/habit/src/intTest/java/de/codecentric/hc/habit/provider/verification/UiPactVerificationTest.java @@ -7,6 +7,7 @@ import au.com.dius.pact.provider.junitsupport.State; import au.com.dius.pact.provider.junitsupport.loader.PactFolder; import de.codecentric.hc.habit.habits.Habit.Schedule.Frequency; +import java.util.UUID; import org.apache.hc.core5.http.HttpRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -56,13 +57,22 @@ public void cleanUp() { @State("habits 'Jogging', 'Meditate' and 'Play guitar' exist") public void createHabitsJoggingAndMeditateAndPlayGuitar() { - jdbcTemplate.update(INSERT_STATEMENT, 1, "Jogging", 2, Frequency.WEEKLY.name(), USER_ID); - jdbcTemplate.update(INSERT_STATEMENT, 51, "Play guitar", 5, Frequency.MONTHLY.name(), USER_ID); - jdbcTemplate.update(INSERT_STATEMENT, 101, "Meditate", 1, Frequency.DAILY.name(), USER_ID); + jdbcTemplate.update( + INSERT_STATEMENT, UUID.randomUUID(), "Jogging", 2, Frequency.WEEKLY.name(), USER_ID); + jdbcTemplate.update( + INSERT_STATEMENT, UUID.randomUUID(), "Play guitar", 5, Frequency.MONTHLY.name(), USER_ID); + jdbcTemplate.update( + INSERT_STATEMENT, UUID.randomUUID(), "Meditate", 1, Frequency.DAILY.name(), USER_ID); } - @State("habit with id '123' exists") + @State("habit with id 'd712645f-cd4f-40c4-b171-bb2ea72d180d' exists") public void createHabit123() { - jdbcTemplate.update(INSERT_STATEMENT, 123, "Habit name", 1, Frequency.DAILY.name(), USER_ID); + jdbcTemplate.update( + INSERT_STATEMENT, + UUID.fromString("d712645f-cd4f-40c4-b171-bb2ea72d180d"), + "Habit name", + 1, + Frequency.DAILY.name(), + USER_ID); } } diff --git a/services/habit/src/intTest/java/de/codecentric/hc/habit/testing/CustomMatchers.java b/services/habit/src/intTest/java/de/codecentric/hc/habit/testing/CustomMatchers.java new file mode 100644 index 00000000..3337f008 --- /dev/null +++ b/services/habit/src/intTest/java/de/codecentric/hc/habit/testing/CustomMatchers.java @@ -0,0 +1,34 @@ +package de.codecentric.hc.habit.testing; + +import java.util.UUID; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +public class CustomMatchers { + + public static UuidMatcher isValidUuid() { + return new UuidMatcher(); + } + + public static class UuidMatcher extends TypeSafeMatcher { + @Override + protected boolean matchesSafely(String item) { + try { + //noinspection ResultOfMethodCallIgnored + UUID.fromString(item); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public void describeTo(Description description) { + description.appendText("a valid UUID string"); + } + + public static UuidMatcher isValidUuid() { + return new UuidMatcher(); + } + } +} diff --git a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java index da98d886..4238730b 100644 --- a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java +++ b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java @@ -4,10 +4,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import jakarta.validation.Valid; @@ -15,6 +12,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; +import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -39,10 +37,7 @@ public class Habit extends AbstractAggregateRoot { public static final String CONSTRAINT_NAME_UNIQUE_NAME = "unique_habit_name"; - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "habit_id_generator") - @SequenceGenerator(name = "habit_id_generator", sequenceName = "habit_seq") - private Long id; + @Id private UUID id; @NotBlank @Size(max = 64) @@ -80,6 +75,7 @@ public enum Frequency { public static Habit from(ModificationRequest modificationRequest, String userId) { Habit habit = builder() + .id(UUID.randomUUID()) .name(modificationRequest.getName()) .schedule(modificationRequest.getSchedule()) .userId(userId) @@ -112,7 +108,7 @@ public static class ModificationRequest { } public record HabitCreated( - Long habitId, String name, Schedule.Frequency frequency, Integer repetitions) { + UUID habitId, String name, Schedule.Frequency frequency, Integer repetitions) { public String getId() { return habitId.toString(); } diff --git a/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java b/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java index d90c66dc..abc63b96 100644 --- a/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java +++ b/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.validation.Valid; import java.net.URI; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.dao.DataAccessException; @@ -46,7 +47,7 @@ public Iterable getHabits(@Parameter(hidden = true) @UserId String userId security = {@SecurityRequirement(name = "User-ID"), @SecurityRequirement(name = "basicAuth")}) @GetMapping("/habits/{id}") @ResponseBody - public Habit getHabit(@PathVariable Long id, @Parameter(hidden = true) @UserId String userId) { + public Habit getHabit(@PathVariable UUID id, @Parameter(hidden = true) @UserId String userId) { return repository .findByIdAndUserId(id, userId) .orElseThrow( @@ -83,7 +84,7 @@ public ResponseEntity createHabit( security = {@SecurityRequirement(name = "User-ID"), @SecurityRequirement(name = "basicAuth")}) @DeleteMapping("/habits/{id}") public ResponseEntity deleteHabit( - @PathVariable Long id, @Parameter(hidden = true) @UserId String userId) { + @PathVariable UUID id, @Parameter(hidden = true) @UserId String userId) { Long deletedRecords = repository.deleteByIdAndUserId(id, userId); if (deletedRecords < 1) { throw new ResponseStatusException( diff --git a/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitRepository.java b/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitRepository.java index f0e2cc99..c51d8e5b 100644 --- a/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitRepository.java +++ b/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitRepository.java @@ -2,15 +2,16 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.transaction.annotation.Transactional; public interface HabitRepository extends JpaRepository { - Optional findByIdAndUserId(Long id, String userId); + Optional findByIdAndUserId(UUID id, String userId); List findAllByUserIdOrderByNameAsc(String userId); @Transactional - Long deleteByIdAndUserId(Long id, String userId); + Long deleteByIdAndUserId(UUID id, String userId); } diff --git a/services/habit/src/main/resources/db/changelog/db.changelog-1.0-create-schema.yaml b/services/habit/src/main/resources/db/changelog/db.changelog-1.0-create-schema.yaml index a4c9beee..1185a330 100644 --- a/services/habit/src/main/resources/db/changelog/db.changelog-1.0-create-schema.yaml +++ b/services/habit/src/main/resources/db/changelog/db.changelog-1.0-create-schema.yaml @@ -8,7 +8,7 @@ databaseChangeLog: columns: - column: name: id - type: BIGINT + type: UUID - column: name: name type: VARCHAR(64) @@ -41,4 +41,4 @@ databaseChangeLog: incrementBy: 50 cycle: false - tagDatabase: - tag: version_1.0 \ No newline at end of file + tag: version_1.0 diff --git a/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java b/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java index 912db203..ff8a8593 100644 --- a/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java +++ b/services/habit/src/test/java/de/codecentric/hc/habit/auth/AuthTest.java @@ -20,6 +20,7 @@ import jakarta.validation.ConstraintViolationException; import java.util.Arrays; import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -45,7 +46,7 @@ public class AuthTest { private static final List DEFAULT_HABITS = Arrays.asList( Habit.builder() - .id(123l) + .id(UUID.randomUUID()) .name("ABC") .userId(DEFAULT_USER) .schedule(DEFAULT_SCHEDULE) diff --git a/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java b/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java index 0fc80ddd..80d39c42 100644 --- a/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java +++ b/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java @@ -4,6 +4,7 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -15,6 +16,7 @@ import java.sql.SQLException; import java.util.List; import java.util.Optional; +import java.util.UUID; import org.hibernate.exception.ConstraintViolationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,7 +35,7 @@ public class HabitControllerTest { private HabitController controller; private HabitRepository repository; private static final String userId = "dummy"; - private static final Long habitId = 123l; + private static final UUID habitId = UUID.randomUUID(); private static final Habit DEFAULT_HABIT = Habit.builder() .id(habitId) @@ -69,7 +71,7 @@ public void getHabitShouldThrowAnExceptionWhenHabitNotFound() { when(repository.findByIdAndUserId(eq(habitId), eq(userId))).thenReturn(Optional.empty()); assertThatExceptionOfType(ResponseStatusException.class) .isThrownBy(() -> controller.getHabit(habitId, userId)) - .withMessage("404 NOT_FOUND \"Habit '123' could not be found.\""); + .withMessageMatching("404 NOT_FOUND \"Habit '(.*)' could not be found.\""); } @Test @@ -82,7 +84,7 @@ public void createHabitShouldCreateHabit() { .path("/{id}") .buildAndExpand(habitId) .toUri(); - when(repository.save(eq(Habit.from(DEFAULT_MODIFICATION, userId)))).thenReturn(DEFAULT_HABIT); + when(repository.save(any())).thenReturn(DEFAULT_HABIT); assertThat(controller.createHabit(DEFAULT_MODIFICATION, userId)) .isEqualTo(ResponseEntity.created(expectedLocation).build()); } @@ -90,7 +92,7 @@ public void createHabitShouldCreateHabit() { @Test public void createHabitShouldHandleDataAccessException() { DataAccessException exception = new DataRetrievalFailureException("UNEXPECTED"); - when(repository.save(eq(Habit.from(DEFAULT_MODIFICATION, userId)))).thenThrow(exception); + when(repository.save(any())).thenThrow(exception); assertThatExceptionOfType(ResponseStatusException.class) .isThrownBy(() -> controller.createHabit(DEFAULT_MODIFICATION, userId)) .withMessage("500 INTERNAL_SERVER_ERROR \"An unexpected database exception occurred.\""); @@ -100,7 +102,7 @@ public void createHabitShouldHandleDataAccessException() { public void createHabitShouldHandleDataIntegrityViolationException() { DataAccessException exception = new DataIntegrityViolationException("ABC", new Exception("other cause")); - when(repository.save(eq(Habit.from(DEFAULT_MODIFICATION, userId)))).thenThrow(exception); + when(repository.save(any())).thenThrow(exception); assertThatExceptionOfType(ResponseStatusException.class) .isThrownBy(() -> controller.createHabit(DEFAULT_MODIFICATION, userId)) .withMessage("500 INTERNAL_SERVER_ERROR \"An unexpected database exception occurred.\""); @@ -112,7 +114,7 @@ public void createHabitShouldHandleConstraintViolationException() { new DataIntegrityViolationException( "ABC", new ConstraintViolationException("DEF", mock(SQLException.class), "other_constraint")); - when(repository.save(eq(Habit.from(DEFAULT_MODIFICATION, userId)))).thenThrow(exception); + when(repository.save(any())).thenThrow(exception); assertThatExceptionOfType(ResponseStatusException.class) .isThrownBy(() -> controller.createHabit(DEFAULT_MODIFICATION, userId)) .withMessage("500 INTERNAL_SERVER_ERROR \"An unexpected database exception occurred.\""); @@ -125,7 +127,7 @@ public void createHabitShouldHandleUniqueHabitNameConstraintViolationException() "ABC", new ConstraintViolationException( "DEF", mock(SQLException.class), Habit.CONSTRAINT_NAME_UNIQUE_NAME)); - when(repository.save(eq(Habit.from(DEFAULT_MODIFICATION, userId)))).thenThrow(exception); + when(repository.save(any())).thenThrow(exception); assertThatExceptionOfType(ResponseStatusException.class) .isThrownBy(() -> controller.createHabit(DEFAULT_MODIFICATION, userId)) .withMessage("400 BAD_REQUEST \"Please choose a unique habit name.\""); @@ -142,6 +144,6 @@ public void deleteHabitShouldThrowExceptionWhenHabitNotFound() { when(repository.deleteByIdAndUserId(eq(habitId), eq(userId))).thenReturn(0l); assertThatExceptionOfType(ResponseStatusException.class) .isThrownBy(() -> controller.deleteHabit(habitId, userId)) - .withMessage("404 NOT_FOUND \"Habit '123' could not be found.\""); + .withMessageMatching("404 NOT_FOUND \"Habit '(.*)' could not be found.\""); } } diff --git a/services/report/src/main/kotlin/de/codecentric/hc/report/Habit.kt b/services/report/src/main/kotlin/de/codecentric/hc/report/Habit.kt index d82eade0..9f05e280 100644 --- a/services/report/src/main/kotlin/de/codecentric/hc/report/Habit.kt +++ b/services/report/src/main/kotlin/de/codecentric/hc/report/Habit.kt @@ -1,9 +1,11 @@ package de.codecentric.hc.report -data class Habit(val id: Long, val schedule: Schedule) +import java.util.UUID + +data class Habit(val id: UUID, val schedule: Schedule) data class Schedule(val repetitions: Int, val frequency: Frequency) enum class Frequency { - DAILY, WEEKLY, MONTHLY, YEARLY + DAILY, WEEKLY, MONTHLY, YEARLY } diff --git a/services/report/src/main/kotlin/de/codecentric/hc/report/HabitTrackingService.kt b/services/report/src/main/kotlin/de/codecentric/hc/report/HabitTrackingService.kt index 5d496704..4464fdaa 100644 --- a/services/report/src/main/kotlin/de/codecentric/hc/report/HabitTrackingService.kt +++ b/services/report/src/main/kotlin/de/codecentric/hc/report/HabitTrackingService.kt @@ -4,15 +4,16 @@ import org.springframework.stereotype.Service import org.springframework.web.client.RestTemplate import org.springframework.web.client.getForObject import java.time.LocalDate +import java.util.UUID @Service class HabitTrackingService( - val habitTrackingProperties: HabitTrackingProperties, - val restTemplate: RestTemplate + val habitTrackingProperties: HabitTrackingProperties, + val restTemplate: RestTemplate ) { - private val trackEndpointUrl: String get() = "${habitTrackingProperties.serviceUrl}/track" + private val trackEndpointUrl: String get() = "${habitTrackingProperties.serviceUrl}/track" - fun getTrackingDates(habitId: Long): List = - restTemplate.getForObject>("$trackEndpointUrl/habits/$habitId").asList() -} \ No newline at end of file + fun getTrackingDates(habitId: UUID): List = + restTemplate.getForObject>("$trackEndpointUrl/habits/$habitId").asList() +} diff --git a/services/report/src/test/kotlin/de/codecentric/hc/report/HabitServiceTest.kt b/services/report/src/test/kotlin/de/codecentric/hc/report/HabitServiceTest.kt index 927852f4..37c268ff 100644 --- a/services/report/src/test/kotlin/de/codecentric/hc/report/HabitServiceTest.kt +++ b/services/report/src/test/kotlin/de/codecentric/hc/report/HabitServiceTest.kt @@ -10,31 +10,32 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.web.client.RestTemplate import org.springframework.web.client.getForObject +import java.util.UUID @ExtendWith(MockKExtension::class) internal class HabitServiceTest { - @MockK - lateinit var restTemplate: RestTemplate + @MockK + lateinit var restTemplate: RestTemplate - @MockK - lateinit var properties: HabitProperties + @MockK + lateinit var properties: HabitProperties - @InjectMockKs - lateinit var subject: HabitService + @InjectMockKs + lateinit var subject: HabitService - @BeforeEach - internal fun setUp() { - every { properties.serviceUrl } returns "url" - } + @BeforeEach + internal fun setUp() { + every { properties.serviceUrl } returns "url" + } - @Test - fun `should call habit endpoint`() { - val habit = Habit(1, Schedule(1, Frequency.WEEKLY)) + @Test + fun `should call habit endpoint`() { + val habit = Habit(UUID.randomUUID(), Schedule(1, Frequency.WEEKLY)) - every { restTemplate.getForObject>("url/habits") } returns arrayOf(habit) + every { restTemplate.getForObject>("url/habits") } returns arrayOf(habit) - val habits = subject.getHabits() - assertThat(habits).isEqualTo(listOf(habit)) - } -} \ No newline at end of file + val habits = subject.getHabits() + assertThat(habits).isEqualTo(listOf(habit)) + } +} diff --git a/services/report/src/test/kotlin/de/codecentric/hc/report/HabitTrackingServiceTest.kt b/services/report/src/test/kotlin/de/codecentric/hc/report/HabitTrackingServiceTest.kt index 503712d9..65fb70be 100644 --- a/services/report/src/test/kotlin/de/codecentric/hc/report/HabitTrackingServiceTest.kt +++ b/services/report/src/test/kotlin/de/codecentric/hc/report/HabitTrackingServiceTest.kt @@ -11,31 +11,35 @@ import org.junit.jupiter.api.extension.ExtendWith import org.springframework.web.client.RestTemplate import org.springframework.web.client.getForObject import java.time.LocalDate +import java.util.UUID @ExtendWith(MockKExtension::class) internal class HabitTrackingServiceTest { - @MockK - lateinit var restTemplate: RestTemplate + @MockK + lateinit var restTemplate: RestTemplate - @MockK - lateinit var properties: HabitTrackingProperties + @MockK + lateinit var properties: HabitTrackingProperties - @InjectMockKs - lateinit var subject: HabitTrackingService + @InjectMockKs + lateinit var subject: HabitTrackingService - @BeforeEach - internal fun setUp() { - every { properties.serviceUrl } returns "url" - } + @BeforeEach + internal fun setUp() { + every { properties.serviceUrl } returns "url" + } - @Test - fun `should call habit tracking endpoint with habit ID`() { - val trackDate = LocalDate.of(2020, 1, 1) - every { restTemplate.getForObject>("url/track/habits/1") } returns arrayOf(trackDate) + @Test + fun `should call habit tracking endpoint with habit ID`() { + val trackDate = LocalDate.of(2020, 1, 1) + val habitId = UUID.fromString("d712645f-cd4f-40c4-b171-bb2ea72d180d") + every { restTemplate.getForObject>("url/track/habits/$habitId") } returns arrayOf( + trackDate + ) - val trackingDates = subject.getTrackingDates(1) + val trackingDates = subject.getTrackingDates(habitId) - assertThat(trackingDates).isEqualTo(listOf(trackDate)) - } -} \ No newline at end of file + assertThat(trackingDates).isEqualTo(listOf(trackDate)) + } +} diff --git a/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitServiceTest.kt b/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitServiceTest.kt index 86ac2f1c..d9ad4e61 100644 --- a/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitServiceTest.kt +++ b/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitServiceTest.kt @@ -14,45 +14,46 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.time.LocalDate +import java.util.UUID @ExtendWith(MockKExtension::class) internal class TrackedHabitServiceTest { - @MockK - lateinit var habitService: HabitService - - @MockK - lateinit var habitTrackingService: HabitTrackingService - - @InjectMockKs - lateinit var subject: TrackedHabitService - - @Nested - inner class `given 1 habit of each schedule frequency with 1 track each` { - - @BeforeEach - internal fun setUp() { - every { habitService.getHabits() } returns listOf( - Habit(1, Schedule(1, DAILY)), - Habit(2, Schedule(1, WEEKLY)), - Habit(3, Schedule(1, MONTHLY)), - Habit(4, Schedule(1, YEARLY)) - ) - every { habitTrackingService.getTrackingDates(any()) } returns listOf(LocalDate.parse("2020-01-01")) - } - - @Test - internal fun `given daily frequency, return 1 tracked habit with one track`() { - val trackedHabits = subject.getTrackedHabitsWith(setOf(DAILY)) - assertThat(trackedHabits.size).isEqualTo(1) - assertThat(trackedHabits.first().tracks.size).isEqualTo(1) - } - - @Test - internal fun `given daily, weekly and monthly frequency, return 3 tracked habits with one track each`() { - val trackedHabits = subject.getTrackedHabitsWith(setOf(DAILY, WEEKLY, MONTHLY)) - assertThat(trackedHabits.size).isEqualTo(3) - assertThat(trackedHabits).allMatch { it.tracks.size == 1 } - } + @MockK + lateinit var habitService: HabitService + + @MockK + lateinit var habitTrackingService: HabitTrackingService + + @InjectMockKs + lateinit var subject: TrackedHabitService + + @Nested + inner class `given 1 habit of each schedule frequency with 1 track each` { + + @BeforeEach + internal fun setUp() { + every { habitService.getHabits() } returns listOf( + Habit(UUID.randomUUID(), Schedule(1, DAILY)), + Habit(UUID.randomUUID(), Schedule(1, WEEKLY)), + Habit(UUID.randomUUID(), Schedule(1, MONTHLY)), + Habit(UUID.randomUUID(), Schedule(1, YEARLY)) + ) + every { habitTrackingService.getTrackingDates(any()) } returns listOf(LocalDate.parse("2020-01-01")) + } + + @Test + internal fun `given daily frequency, return 1 tracked habit with one track`() { + val trackedHabits = subject.getTrackedHabitsWith(setOf(DAILY)) + assertThat(trackedHabits.size).isEqualTo(1) + assertThat(trackedHabits.first().tracks.size).isEqualTo(1) + } + + @Test + internal fun `given daily, weekly and monthly frequency, return 3 tracked habits with one track each`() { + val trackedHabits = subject.getTrackedHabitsWith(setOf(DAILY, WEEKLY, MONTHLY)) + assertThat(trackedHabits.size).isEqualTo(3) + assertThat(trackedHabits).allMatch { it.tracks.size == 1 } } -} \ No newline at end of file + } +} diff --git a/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitTest.kt b/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitTest.kt index 142f2d56..d35c0920 100644 --- a/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitTest.kt +++ b/services/report/src/test/kotlin/de/codecentric/hc/report/TrackedHabitTest.kt @@ -5,198 +5,198 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import java.time.LocalDate +import java.util.UUID class TrackedHabitTest { + @Nested + inner class `get scheduled repetitions for period` { + @Nested - inner class `get scheduled repetitions for period` { - - @Nested - inner class `given daily habit` { - - @Test - fun `given one repetition and 7-day period, should return 7`() { - val trackedHabit = createTrackedHabit(1, Frequency.DAILY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 7) - ) - assertThat(result).isEqualTo(7) - } - - @Test - fun `given one repetition and 30-day period, should return 30`() { - val trackedHabit = createTrackedHabit(1, Frequency.DAILY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 30) - ) - assertThat(result).isEqualTo(30) - } - - @Test - fun `given two repetitions and 30-day period, should return 60`() { - val trackedHabit = createTrackedHabit(2, Frequency.DAILY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 30) - ) - assertThat(result).isEqualTo(60) - } - } - - @Nested - inner class `given weekly habit` { - @Test - fun `given one repetition and 7-day period, should return 1`() { - val trackedHabit = createTrackedHabit(1, Frequency.WEEKLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 7) - ) - assertThat(result).isEqualTo(1) - } - - @Test - fun `given one repetition and 30-day period, should return 4`() { - val trackedHabit = createTrackedHabit(1, Frequency.WEEKLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 30) - ) - assertThat(result).isEqualTo(4) - } - - @Test - fun `given two repetitions and 7-day period, should return 2`() { - val trackedHabit = createTrackedHabit(2, Frequency.WEEKLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 7) - ) - assertThat(result).isEqualTo(2) - } - - @Test - fun `given two repetitions and 30-day period, should return 9`() { - val trackedHabit = createTrackedHabit(2, Frequency.WEEKLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 30) - ) - assertThat(result).isEqualTo(9) - } - } - - @Nested - inner class `given monthly habit` { - - @Test - fun `given one repetition and 30-day period, should return 1`() { - val trackedHabit = createTrackedHabit(1, Frequency.MONTHLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 30) - ) - assertThat(result).isEqualTo(1) - } - - @Test - fun `given one repetition and 7-day period, should return 0`() { - val trackedHabit = createTrackedHabit(1, Frequency.MONTHLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 7) - ) - assertThat(result).isEqualTo(0) - } - - @Test - fun `given four repetitions and 7-day period, should return 1`() { - val trackedHabit = createTrackedHabit(4, Frequency.MONTHLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 7) - ) - assertThat(result).isEqualTo(1) - } - - @Test - fun `given two repetitions and 30-day period, should return 2`() { - val trackedHabit = createTrackedHabit(2, Frequency.MONTHLY) - val result = - trackedHabit.getScheduledRepetitionsForPeriod( - LocalDate.of(2020, 1, 1).. - LocalDate.of(2020, 1, 30) - ) - assertThat(result).isEqualTo(2) - } - } + inner class `given daily habit` { + + @Test + fun `given one repetition and 7-day period, should return 7`() { + val trackedHabit = createTrackedHabit(1, Frequency.DAILY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 7) + ) + assertThat(result).isEqualTo(7) + } + + @Test + fun `given one repetition and 30-day period, should return 30`() { + val trackedHabit = createTrackedHabit(1, Frequency.DAILY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 30) + ) + assertThat(result).isEqualTo(30) + } + + @Test + fun `given two repetitions and 30-day period, should return 60`() { + val trackedHabit = createTrackedHabit(2, Frequency.DAILY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 30) + ) + assertThat(result).isEqualTo(60) + } } @Nested - inner class `get tracked repetitions for period` { - - @Test - internal fun `given one track is in date range, should return 1`() { - val trackedHabit = createTrackedHabit(tracks = listOf(LocalDate.parse("2020-01-05"))) - - val result = trackedHabit.getTrackedRepetitionsForPeriod( - LocalDate.parse("2020-01-04").. - LocalDate.parse("2020-01-06") - ) - assertThat(result).isEqualTo(1) - } - - @Test - internal fun `given three tracks are in date range, should return 3`() { - val trackedHabit = createTrackedHabit( - tracks = listOf( - LocalDate.parse("2020-01-05"), - LocalDate.parse("2020-01-06"), - LocalDate.parse("2020-01-07") - ) - ) - - val result = trackedHabit.getTrackedRepetitionsForPeriod( - LocalDate.parse("2020-01-04")..LocalDate.parse("2020-01-07") - ) - assertThat(result).isEqualTo(3) - } - - @Test - internal fun `given two of three tracks are in date range, should return 2`() { - val trackedHabit = createTrackedHabit( - tracks = listOf( - LocalDate.parse("2020-01-01"), - LocalDate.parse("2020-01-06"), - LocalDate.parse("2020-01-07") - ) - ) - - val result = trackedHabit.getTrackedRepetitionsForPeriod( - LocalDate.parse("2020-01-04").. - LocalDate.parse("2020-01-07") - ) - assertThat(result).isEqualTo(2) - } + inner class `given weekly habit` { + @Test + fun `given one repetition and 7-day period, should return 1`() { + val trackedHabit = createTrackedHabit(1, Frequency.WEEKLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 7) + ) + assertThat(result).isEqualTo(1) + } + + @Test + fun `given one repetition and 30-day period, should return 4`() { + val trackedHabit = createTrackedHabit(1, Frequency.WEEKLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 30) + ) + assertThat(result).isEqualTo(4) + } + + @Test + fun `given two repetitions and 7-day period, should return 2`() { + val trackedHabit = createTrackedHabit(2, Frequency.WEEKLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 7) + ) + assertThat(result).isEqualTo(2) + } + + @Test + fun `given two repetitions and 30-day period, should return 9`() { + val trackedHabit = createTrackedHabit(2, Frequency.WEEKLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 30) + ) + assertThat(result).isEqualTo(9) + } } - fun createTrackedHabit( - repetitions: Int = 1, - frequency: Frequency = Frequency.DAILY, - tracks: Collection = emptyList() - ) = - TrackedHabit(Habit(0, Schedule(repetitions, frequency)), tracks) -} + @Nested + inner class `given monthly habit` { + + @Test + fun `given one repetition and 30-day period, should return 1`() { + val trackedHabit = createTrackedHabit(1, Frequency.MONTHLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 30) + ) + assertThat(result).isEqualTo(1) + } + + @Test + fun `given one repetition and 7-day period, should return 0`() { + val trackedHabit = createTrackedHabit(1, Frequency.MONTHLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 7) + ) + assertThat(result).isEqualTo(0) + } + + @Test + fun `given four repetitions and 7-day period, should return 1`() { + val trackedHabit = createTrackedHabit(4, Frequency.MONTHLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 7) + ) + assertThat(result).isEqualTo(1) + } + + @Test + fun `given two repetitions and 30-day period, should return 2`() { + val trackedHabit = createTrackedHabit(2, Frequency.MONTHLY) + val result = + trackedHabit.getScheduledRepetitionsForPeriod( + LocalDate.of(2020, 1, 1).. + LocalDate.of(2020, 1, 30) + ) + assertThat(result).isEqualTo(2) + } + } + } + + @Nested + inner class `get tracked repetitions for period` { + + @Test + internal fun `given one track is in date range, should return 1`() { + val trackedHabit = createTrackedHabit(tracks = listOf(LocalDate.parse("2020-01-05"))) + val result = trackedHabit.getTrackedRepetitionsForPeriod( + LocalDate.parse("2020-01-04").. + LocalDate.parse("2020-01-06") + ) + assertThat(result).isEqualTo(1) + } + + @Test + internal fun `given three tracks are in date range, should return 3`() { + val trackedHabit = createTrackedHabit( + tracks = listOf( + LocalDate.parse("2020-01-05"), + LocalDate.parse("2020-01-06"), + LocalDate.parse("2020-01-07") + ) + ) + + val result = trackedHabit.getTrackedRepetitionsForPeriod( + LocalDate.parse("2020-01-04")..LocalDate.parse("2020-01-07") + ) + assertThat(result).isEqualTo(3) + } + + @Test + internal fun `given two of three tracks are in date range, should return 2`() { + val trackedHabit = createTrackedHabit( + tracks = listOf( + LocalDate.parse("2020-01-01"), + LocalDate.parse("2020-01-06"), + LocalDate.parse("2020-01-07") + ) + ) + + val result = trackedHabit.getTrackedRepetitionsForPeriod( + LocalDate.parse("2020-01-04").. + LocalDate.parse("2020-01-07") + ) + assertThat(result).isEqualTo(2) + } + } + + fun createTrackedHabit( + repetitions: Int = 1, + frequency: Frequency = Frequency.DAILY, + tracks: Collection = emptyList() + ) = + TrackedHabit(Habit(UUID.randomUUID(), Schedule(repetitions, frequency)), tracks) +} diff --git a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitModuleIntegrationTest.java b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitModuleIntegrationTest.java index 7a7d2a35..be1e6534 100644 --- a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitModuleIntegrationTest.java +++ b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitModuleIntegrationTest.java @@ -5,6 +5,7 @@ import de.codecentric.habitcentric.track.auth.UserIdArgumentResolver; import java.time.LocalDate; import java.util.Set; +import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -28,20 +29,21 @@ void tearDown() { @Test void shouldPublishDateTrackedEventWhenHabitTrackingIsSaved(Scenario scenario) { + UUID habitId = UUID.fromString("d712645f-cd4f-40c4-b171-bb2ea72d180d"); habitTrackingRepository.save( - HabitTracking.from("userId", 1L, Set.of(LocalDate.parse("2023-09-29")))); + HabitTracking.from("userId", habitId, Set.of(LocalDate.parse("2023-09-29")))); scenario .stimulate( () -> habitTrackingController.putHabitTrackingRecords( "userId", - 1L, + "d712645f-cd4f-40c4-b171-bb2ea72d180d", Set.of(LocalDate.parse("2023-09-29"), LocalDate.parse("2023-09-30")))) .andWaitForEventOfType(HabitTracking.DateTracked.class) .toArriveAndVerify( event -> { - assertThat(event.habitId()).isEqualTo(1L); + assertThat(event.habitId()).isEqualTo(habitId); assertThat(event.userId()).isEqualTo("userId"); assertThat(event.trackDate()).isEqualTo(LocalDate.parse("2023-09-30")); }); @@ -49,19 +51,24 @@ void shouldPublishDateTrackedEventWhenHabitTrackingIsSaved(Scenario scenario) { @Test void shouldPublishDateUntrackedEventWhenExistingHabitTrackingIsRemoved(Scenario scenario) { + UUID habitId = UUID.fromString("d712645f-cd4f-40c4-b171-bb2ea72d180d"); habitTrackingRepository.save( HabitTracking.from( - "userId", 1L, Set.of(LocalDate.parse("2023-09-29"), LocalDate.parse("2023-09-30")))); + "userId", + habitId, + Set.of(LocalDate.parse("2023-09-29"), LocalDate.parse("2023-09-30")))); scenario .stimulate( () -> habitTrackingController.putHabitTrackingRecords( - "userId", 1L, Set.of(LocalDate.parse("2023-09-29")))) + "userId", + "d712645f-cd4f-40c4-b171-bb2ea72d180d", + Set.of(LocalDate.parse("2023-09-29")))) .andWaitForEventOfType(HabitTracking.DateUntracked.class) .toArriveAndVerify( event -> { - assertThat(event.habitId()).isEqualTo(1L); + assertThat(event.habitId()).isEqualTo(habitId); assertThat(event.userId()).isEqualTo("userId"); assertThat(event.trackDate()).isEqualTo(LocalDate.parse("2023-09-30")); }); diff --git a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerRestAssuredTest.java b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerRestAssuredTest.java index e2f78a83..0b50d350 100644 --- a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerRestAssuredTest.java +++ b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerRestAssuredTest.java @@ -11,6 +11,7 @@ import de.codecentric.habitcentric.track.RestAssuredTest; import java.time.LocalDate; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -24,7 +25,7 @@ public class HabitTrackingControllerRestAssuredTest extends RestAssuredTest { private final String urlTemplate = "/track/users/{userId}/habits/{habitId}"; private final String userId = "abc.def"; - private final Long habitId = 123L; + private final UUID habitId = UUID.randomUUID(); @Autowired private JdbcTemplate jdbcTemplate; @@ -190,10 +191,10 @@ public void shouldRejectGetRequestsWithUserIdLongerThan64Characters() { } @Test - public void shouldRejectGetRequestsWithoutPositiveHabitId() { + public void shouldRejectGetRequestsWithoutUuidHabitId() { given() .when() - .get(urlTemplate, userId, 0) + .get(urlTemplate, userId, "abcd") .then() .statusCode(400) .contentType(JSON) @@ -227,12 +228,12 @@ public void shouldRejectPutRequestsWithUserIdLongerThan64Characters() { } @Test - public void shouldRejectPutRequestsWithoutPositiveHabitId() { + public void shouldRejectPutRequestsWithoutUuidHabitId() { given() .contentType(JSON) .body(new LocalDate[0]) .when() - .put(urlTemplate, userId, 0) + .put(urlTemplate, userId, "abcd") .then() .statusCode(400) .contentType(JSON) @@ -244,7 +245,7 @@ public void shouldRejectPutRequestsWithInvalidContentType() { given() .body(new LocalDate[0]) .when() - .put(urlTemplate, userId, 0) + .put(urlTemplate, userId, "d712645f-cd4f-40c4-b171-bb2ea72d180d") .then() .statusCode(415) .contentType(JSON) @@ -257,7 +258,7 @@ public void shouldRejectPutRequestsWithoutBody() { given() .contentType(JSON) .when() - .put(urlTemplate, userId, 0) + .put(urlTemplate, userId, "d712645f-cd4f-40c4-b171-bb2ea72d180d") .then() .statusCode(400) .contentType(JSON) @@ -272,7 +273,7 @@ public void shouldRejectPutRequestsWithoutInvalidDates() { .contentType(JSON) .body(body) .when() - .put(urlTemplate, userId, 0) + .put(urlTemplate, userId, "d712645f-cd4f-40c4-b171-bb2ea72d180d") .then() .statusCode(400) .contentType(JSON) diff --git a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtRestAssuredTest.java b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtRestAssuredTest.java index 05f36504..9f762b52 100644 --- a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtRestAssuredTest.java +++ b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtRestAssuredTest.java @@ -10,6 +10,7 @@ import de.codecentric.habitcentric.track.RestAssuredTest; import io.restassured.specification.RequestSpecification; import java.time.LocalDate; +import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -23,7 +24,7 @@ public class HabitTrackingControllerJwtRestAssuredTest extends RestAssuredTest { private final String urlTemplate = "/track/habits/{habitId}"; - private final Long habitId = 123L; + private final UUID habitId = UUID.randomUUID(); @Autowired private JdbcTemplate jdbcTemplate; diff --git a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcher.java b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcher.java index 71b88620..72d14df5 100644 --- a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcher.java +++ b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcher.java @@ -12,6 +12,6 @@ public static Matcher hasUserIdViolationError() { } public static Matcher hasHabitIdViolationError() { - return hasError(CONSTRAINT_VIOLATION, "must be greater than 0"); + return hasError(CONSTRAINT_VIOLATION, "must be a valid UUID"); } } diff --git a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcherTest.java b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcherTest.java index 55d1b7c1..bef4bb2e 100644 --- a/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcherTest.java +++ b/services/track/src/intTest/java/de/codecentric/habitcentric/track/habit/matcher/HabitApiMatcherTest.java @@ -13,7 +13,7 @@ public class HabitApiMatcherTest { private final String habitIdViolationErrorResponse = - "{\"errors\":[{\"code\":\"TRACK_101\",\"title\":\"Constraint violation\",\"detail\":\"must be greater than 0\",\"id\":\"123\"}]}"; + "{\"errors\":[{\"code\":\"TRACK_101\",\"title\":\"Constraint violation\",\"detail\":\"must be a valid UUID\",\"id\":\"123\"}]}"; private final String mismatchErrorResponse = "{\"errors\":[{\"code\":\"TRACK_101\",\"title\":\"Constraint violation\",\"detail\":\"MISMATCH\",\"id\":\"123\"}]}"; private final String multipleErrorsResponse = @@ -42,6 +42,6 @@ public void describeToShouldReturnExpectedErrorsAsJson() { hasHabitIdViolationError().describeTo(description); verify(description) .appendText( - "{\"code\":\"TRACK_101\",\"title\":\"Constraint violation\",\"detail\":\"must be greater than 0\"}"); + "{\"code\":\"TRACK_101\",\"title\":\"Constraint violation\",\"detail\":\"must be a valid UUID\"}"); } } diff --git a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTracking.java b/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTracking.java index 6c287d3c..1fc7dfdc 100644 --- a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTracking.java +++ b/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTracking.java @@ -12,7 +12,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import java.io.Serializable; import java.time.LocalDate; @@ -20,6 +19,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -82,11 +82,11 @@ public List getSortedTrackingDates() { return trackings.stream().sorted().toList(); } - public static HabitTracking from(String userId, Long habitId) { + public static HabitTracking from(String userId, UUID habitId) { return new HabitTracking(new Id(userId, habitId), new HashSet<>()); } - public static HabitTracking from(String userId, Long habitId, Collection trackings) { + public static HabitTracking from(String userId, UUID habitId, Collection trackings) { return new HabitTracking(new Id(userId, habitId), new HashSet<>(trackings)); } @@ -102,18 +102,18 @@ public static class Id implements Serializable { @Size(max = 64) private String userId; - @NotNull @Positive private Long habitId; + @NotNull private UUID habitId; } @Externalized("habit-tracking-events::#{#this.getId()}") - public record DateTracked(String userId, Long habitId, LocalDate trackDate) { + public record DateTracked(String userId, UUID habitId, LocalDate trackDate) { public String getId() { return userId + "-" + habitId; } } @Externalized("habit-tracking-events::#{#this.getId()}") - public record DateUntracked(String userId, Long habitId, LocalDate trackDate) { + public record DateUntracked(String userId, UUID habitId, LocalDate trackDate) { public String getId() { return userId + "-" + habitId; } diff --git a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingController.java b/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingController.java index 8e9b052b..3e40ffb2 100644 --- a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingController.java +++ b/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingController.java @@ -1,12 +1,12 @@ package de.codecentric.habitcentric.track.habit; import de.codecentric.habitcentric.track.auth.UserId; -import de.codecentric.habitcentric.track.habit.validation.HabitId; import jakarta.transaction.Transactional; import java.time.LocalDate; import java.util.Collection; import java.util.Collections; import java.util.Set; +import java.util.UUID; import org.springframework.stereotype.Controller; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; @@ -26,22 +26,23 @@ public HabitTrackingController(HabitTrackingRepository repository) { } @Transactional - @PutMapping("/track/habits/{habitId}") + @PutMapping("/track/habits/{habitIdString}") @ResponseBody public Collection putHabitTrackingRecordsWithJwt( @UserId String userId, - @PathVariable @HabitId Long habitId, + @org.hibernate.validator.constraints.UUID @PathVariable String habitIdString, @RequestBody Set dates) { - return putHabitTrackingRecords(userId, habitId, dates); + return putHabitTrackingRecords(userId, habitIdString, dates); } @Transactional - @PutMapping("/track/users/{userId}/habits/{habitId}") + @PutMapping("/track/users/{userId}/habits/{habitIdString}") @ResponseBody public Collection putHabitTrackingRecords( @PathVariable @UserId String userId, - @PathVariable @HabitId Long habitId, + @org.hibernate.validator.constraints.UUID @PathVariable String habitIdString, @RequestBody Set dates) { + var habitId = UUID.fromString(habitIdString); var existingHabitTrackings = repository .findByIdUserIdAndIdHabitId(userId, habitId) @@ -54,19 +55,21 @@ public Collection putHabitTrackingRecords( return existingHabitTrackings.getSortedTrackingDates(); } - @GetMapping("/track/habits/{habitId}") + @GetMapping("/track/habits/{habitIdString}") @ResponseBody public Iterable getHabitTrackingRecordsWithJwt( - @UserId String userId, @PathVariable @HabitId Long habitId) { - return getHabitTrackingRecords(userId, habitId); + @UserId String userId, + @org.hibernate.validator.constraints.UUID @PathVariable String habitIdString) { + return getHabitTrackingRecords(userId, habitIdString); } - @GetMapping("/track/users/{userId}/habits/{habitId}") + @GetMapping("/track/users/{userId}/habits/{habitIdString}") @ResponseBody public Iterable getHabitTrackingRecords( - @PathVariable @UserId String userId, @PathVariable @HabitId Long habitId) { + @PathVariable @UserId String userId, + @org.hibernate.validator.constraints.UUID @PathVariable String habitIdString) { return repository - .findByIdUserIdAndIdHabitId(userId, habitId) + .findByIdUserIdAndIdHabitId(userId, UUID.fromString(habitIdString)) .map(HabitTracking::getSortedTrackingDates) .orElse(Collections.emptyList()); } diff --git a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingRepository.java b/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingRepository.java index 7626cdf7..bb721f51 100644 --- a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingRepository.java +++ b/services/track/src/main/java/de/codecentric/habitcentric/track/habit/HabitTrackingRepository.java @@ -1,8 +1,9 @@ package de.codecentric.habitcentric.track.habit; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; public interface HabitTrackingRepository extends JpaRepository { - Optional findByIdUserIdAndIdHabitId(String userId, Long habitId); + Optional findByIdUserIdAndIdHabitId(String userId, UUID habitId); } diff --git a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/validation/HabitId.java b/services/track/src/main/java/de/codecentric/habitcentric/track/habit/validation/HabitId.java deleted file mode 100644 index 91f0616f..00000000 --- a/services/track/src/main/java/de/codecentric/habitcentric/track/habit/validation/HabitId.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.codecentric.habitcentric.track.habit.validation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; -import jakarta.validation.ReportAsSingleViolation; -import jakarta.validation.constraints.Positive; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Constraint(validatedBy = {}) -@Positive -@ReportAsSingleViolation -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.FIELD, ElementType.PARAMETER}) -public @interface HabitId { - - String message() default "{jakarta.validation.constraints.Positive.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; -} diff --git a/services/track/src/main/resources/db/migration/V1__create-schema.sql b/services/track/src/main/resources/db/migration/V1__create-schema.sql index 26ef597f..9b28d87f 100644 --- a/services/track/src/main/resources/db/migration/V1__create-schema.sql +++ b/services/track/src/main/resources/db/migration/V1__create-schema.sql @@ -1,5 +1,16 @@ -CREATE TABLE habit_tracking (user_id VARCHAR(64) NOT NULL, habit_id BIGINT NOT NULL); -ALTER TABLE habit_tracking ADD PRIMARY KEY (user_id, habit_id); +CREATE TABLE habit_tracking +( + user_id VARCHAR(64) NOT NULL, + habit_id UUID NOT NULL +); +ALTER TABLE habit_tracking + ADD PRIMARY KEY (user_id, habit_id); -CREATE TABLE tracked_dates(tracking_date DATE NOT NULL, user_id VARCHAR(64) NOT NULL, habit_id BIGINT NOT NULL); -ALTER TABLE tracked_dates ADD FOREIGN KEY (user_id, habit_id) REFERENCES habit_tracking(user_id, habit_id); +CREATE TABLE tracked_dates +( + tracking_date DATE NOT NULL, + user_id VARCHAR(64) NOT NULL, + habit_id UUID NOT NULL +); +ALTER TABLE tracked_dates + ADD FOREIGN KEY (user_id, habit_id) REFERENCES habit_tracking (user_id, habit_id); diff --git a/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerWebMvcTest.java b/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerWebMvcTest.java index 5a6ed1d4..9a70abcd 100644 --- a/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerWebMvcTest.java +++ b/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingControllerWebMvcTest.java @@ -13,6 +13,7 @@ import java.time.LocalDate; import java.util.Optional; import java.util.Set; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -28,7 +29,7 @@ public class HabitTrackingControllerWebMvcTest { private final String urlTemplate = "/track/users/{userId}/habits/{habitId}"; private final String userId = "abc.def"; - private final Long habitId = 123L; + private final UUID habitId = UUID.randomUUID(); private final HabitTracking defaultTrackRecords = HabitTracking.from( diff --git a/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingTest.java b/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingTest.java index 61c85a04..ae05ae37 100644 --- a/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingTest.java +++ b/services/track/src/test/java/de/codecentric/habitcentric/track/habit/HabitTrackingTest.java @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Set; +import java.util.UUID; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -12,21 +13,23 @@ class HabitTrackingTest { @Test void trackShouldAddTrackingDates() { - var subject = HabitTracking.from("userId", 1L); + var subject = HabitTracking.from("userId", UUID.randomUUID()); subject.track(Set.of(LocalDate.parse("2023-01-01"))); assertThat(subject.getTrackings()).contains(LocalDate.parse("2023-01-01")); } @Test void trackShouldOverwriteExistingTrackingDates() { - var subject = HabitTracking.from("userId", 1L, List.of(LocalDate.parse("2023-01-01"))); + var subject = + HabitTracking.from("userId", UUID.randomUUID(), List.of(LocalDate.parse("2023-01-01"))); subject.track(Set.of(LocalDate.parse("2023-01-02"))); assertThat(subject.getTrackings()).containsOnly(LocalDate.parse("2023-01-02")); } @Test void trackShouldNotAddTrackingDateIfTrackingDateAlreadyExists() { - var subject = HabitTracking.from("userId", 1L, List.of(LocalDate.parse("2023-01-01"))); + var subject = + HabitTracking.from("userId", UUID.randomUUID(), List.of(LocalDate.parse("2023-01-01"))); subject.track(Set.of(LocalDate.parse("2023-01-01"))); assertThat(subject.getTrackings()).containsOnly(LocalDate.parse("2023-01-01")); } @@ -35,7 +38,9 @@ void trackShouldNotAddTrackingDateIfTrackingDateAlreadyExists() { void getSortedTrackingDatesShouldReturnTrackingDatesSortedAscending() { var subject = HabitTracking.from( - "userId", 1L, List.of(LocalDate.parse("2023-01-02"), LocalDate.parse("2023-01-01"))); + "userId", + UUID.randomUUID(), + List.of(LocalDate.parse("2023-01-02"), LocalDate.parse("2023-01-01"))); assertThat(subject.getSortedTrackingDates()) .containsExactly(LocalDate.parse("2023-01-01"), LocalDate.parse("2023-01-02")); } @@ -44,8 +49,9 @@ void getSortedTrackingDatesShouldReturnTrackingDatesSortedAscending() { class DateTrackedTest { @Test void getIdShouldReturnCombinedId() { - var subject = new HabitTracking.DateTracked("userId", 1L, LocalDate.now()); - assertThat(subject.getId()).isEqualTo("userId-1"); + UUID habitId = UUID.fromString("d712645f-cd4f-40c4-b171-bb2ea72d180d"); + var subject = new HabitTracking.DateTracked("userId", habitId, LocalDate.now()); + assertThat(subject.getId()).isEqualTo("userId-d712645f-cd4f-40c4-b171-bb2ea72d180d"); } } @@ -53,8 +59,9 @@ void getIdShouldReturnCombinedId() { class DateUntrackedTest { @Test void getIdShouldReturnCombinedId() { - var subject = new HabitTracking.DateUntracked("userId", 1L, LocalDate.now()); - assertThat(subject.getId()).isEqualTo("userId-1"); + UUID habitId = UUID.fromString("d712645f-cd4f-40c4-b171-bb2ea72d180d"); + var subject = new HabitTracking.DateUntracked("userId", habitId, LocalDate.now()); + assertThat(subject.getId()).isEqualTo("userId-d712645f-cd4f-40c4-b171-bb2ea72d180d"); } } } diff --git a/services/track/src/test/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtWebMvcTest.java b/services/track/src/test/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtWebMvcTest.java index 4b881e5e..9f800077 100644 --- a/services/track/src/test/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtWebMvcTest.java +++ b/services/track/src/test/java/de/codecentric/habitcentric/track/habit/jwt/HabitTrackingControllerJwtWebMvcTest.java @@ -33,7 +33,7 @@ public class HabitTrackingControllerJwtWebMvcTest { private final String urlTemplate = "/track/habits/{habitId}"; private final String userId = "abc.def"; private final String authorizationHeader = "Bearer _"; - private final Long habitId = 123L; + private final UUID habitId = UUID.randomUUID(); private final HabitTracking defaultTrackRecords = HabitTracking.from( diff --git a/services/ui/src/overview/api/habit/api.ts b/services/ui/src/overview/api/habit/api.ts index a2e9cb50..d5664722 100644 --- a/services/ui/src/overview/api/habit/api.ts +++ b/services/ui/src/overview/api/habit/api.ts @@ -9,7 +9,7 @@ export async function createHabit( name: string, repetitions: number, frequency: Frequency, - mutate: ScopedMutator + mutate: ScopedMutator, ) { const user = getUser(); const request: CreateHabitRequest = { @@ -28,20 +28,20 @@ export async function createHabit( }, body: JSON.stringify(request), }, - user?.access_token + user?.access_token, ); await mutate(["/habits", user?.access_token]); await mutate(["/report/achievement", user?.access_token]); } -export async function deleteHabit(id: number, mutate: ScopedMutator) { +export async function deleteHabit(id: string, mutate: ScopedMutator) { const user = getUser(); await fetchWithToken( `/habits/${id}`, { method: "DELETE", }, - user?.access_token + user?.access_token, ); await mutate(["/habits", user?.access_token]); await mutate(["/report/achievement", user?.access_token]); diff --git a/services/ui/src/overview/api/habit/habit.ts b/services/ui/src/overview/api/habit/habit.ts index a1477040..d314509f 100644 --- a/services/ui/src/overview/api/habit/habit.ts +++ b/services/ui/src/overview/api/habit/habit.ts @@ -1,5 +1,5 @@ export type Habit = { - id: number; + id: string; name: string; schedule: Schedule; }; @@ -13,7 +13,7 @@ export type Frequency = "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY"; export function scheduleToString(schedule: Schedule): string { return `${repetitionsToString(schedule.repetitions)} per ${frequencyToString( - schedule.frequency + schedule.frequency, )}`; } diff --git a/services/ui/src/overview/api/track/api.ts b/services/ui/src/overview/api/track/api.ts index 7d0dff88..b1bde9d2 100644 --- a/services/ui/src/overview/api/track/api.ts +++ b/services/ui/src/overview/api/track/api.ts @@ -4,13 +4,13 @@ import { fetchWithToken } from "../../../auth/fetchWithToken"; import { ScopedMutator } from "swr/_internal"; export async function putTrackedDates( - habitId: number, + habitId: string, trackedDates: Date[], - mutate: ScopedMutator + mutate: ScopedMutator, ) { const user = getUser(); const formattedTrackedDates = trackedDates.map((date) => - formatISO(date, { representation: "date" }) + formatISO(date, { representation: "date" }), ); await fetchWithToken( `/track/habits/${habitId}`, @@ -21,7 +21,7 @@ export async function putTrackedDates( }, body: JSON.stringify(formattedTrackedDates), }, - user?.access_token + user?.access_token, ); await mutate(["/track/habits", habitId, user?.access_token]); await mutate(["/report/achievement", user?.access_token]); diff --git a/services/ui/src/overview/api/track/useTrackedDates.ts b/services/ui/src/overview/api/track/useTrackedDates.ts index 65e43b81..ce6e7b13 100644 --- a/services/ui/src/overview/api/track/useTrackedDates.ts +++ b/services/ui/src/overview/api/track/useTrackedDates.ts @@ -6,11 +6,11 @@ import { fetchWithToken } from "../../../auth/fetchWithToken"; const trackedDatesFetcher: Fetcher = ([url, habitId, token]) => fetchWithToken(`${url}/${habitId}`, {}, token).then((res) => res.json()); -export function useTrackedDatesOfHabit(habitId: number) { +export function useTrackedDatesOfHabit(habitId: string) { const auth = useAuth(); const { data, error } = useSWR( ["/track/habits", habitId, auth.user?.access_token], - trackedDatesFetcher + trackedDatesFetcher, ); const trackedDates = data?.map((dateString) => parseISO(dateString)); diff --git a/services/ui/src/overview/components/list/HabitItem.tsx b/services/ui/src/overview/components/list/HabitItem.tsx index 738e070c..9ff15a10 100644 --- a/services/ui/src/overview/components/list/HabitItem.tsx +++ b/services/ui/src/overview/components/list/HabitItem.tsx @@ -7,7 +7,7 @@ import CardPopover from "../CardPopover"; import TrackDatePicker from "./TrackDatePicker"; export type HabitItemProps = { - id: number; + id: string; name: string; schedule: Schedule; }; @@ -27,7 +27,7 @@ export function HabitItem({ id, name, schedule }: HabitItemProps) { ); } -type TrackPopoverProps = { habitId: number; name: string }; +type TrackPopoverProps = { habitId: string; name: string }; function TrackPopover({ habitId, name }: TrackPopoverProps) { return ( @@ -37,7 +37,7 @@ function TrackPopover({ habitId, name }: TrackPopoverProps) { ); } -type DeleteButtonProps = { id: number; name: string }; +type DeleteButtonProps = { id: string; name: string }; function DeleteButton({ id, name }: DeleteButtonProps) { const { mutate } = useSWRConfig(); diff --git a/services/ui/src/overview/components/list/TrackDatePicker.tsx b/services/ui/src/overview/components/list/TrackDatePicker.tsx index 5ec6ba04..5a6155cd 100644 --- a/services/ui/src/overview/components/list/TrackDatePicker.tsx +++ b/services/ui/src/overview/components/list/TrackDatePicker.tsx @@ -8,7 +8,7 @@ import { putTrackedDates } from "../../api/track/api"; import { useSWRConfig } from "swr"; export type TrackDatePickerProps = { - habitId: number; + habitId: string; }; function TrackDatePicker({ habitId }: TrackDatePickerProps) { @@ -20,12 +20,9 @@ function TrackDatePicker({ habitId }: TrackDatePickerProps) { async function updateTrackedDates(selectedDate: Date) { const updatedTrackedDates = - trackedDates!.findIndex( - (trackedDate) => trackedDate.valueOf() === selectedDate.valueOf() - ) >= 0 - ? trackedDates!.filter( - (trackedDate) => trackedDate.valueOf() !== selectedDate.valueOf() - ) + trackedDates!.findIndex((trackedDate) => trackedDate.valueOf() === selectedDate.valueOf()) >= + 0 + ? trackedDates!.filter((trackedDate) => trackedDate.valueOf() !== selectedDate.valueOf()) : [...trackedDates!, selectedDate]; await putTrackedDates(habitId, updatedTrackedDates, mutate); } From ac651045ed013f7efc02f8abe4a38f99415656dd Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Wed, 3 Jan 2024 14:27:56 +0100 Subject: [PATCH 4/6] feat(habit): publish habit deleted event when habit is deleted --- .../habits/HabitModuleIntegrationTest.java | 41 +++++++++++++++---- .../de/codecentric/hc/habit/habits/Habit.java | 15 ++++++- .../hc/habit/habits/HabitController.java | 23 +++++++---- .../hc/habit/habits/HabitControllerTest.java | 12 +++--- 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java index a4009f32..9391a823 100644 --- a/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java +++ b/services/habit/src/intTest/java/de/codecentric/hc/habit/habits/HabitModuleIntegrationTest.java @@ -1,8 +1,11 @@ package de.codecentric.hc.habit.habits; +import static de.codecentric.hc.habit.habits.Habit.Schedule.Frequency.WEEKLY; import static org.assertj.core.api.Assertions.assertThat; import de.codecentric.hc.habit.auth.UserIdArgumentResolver; +import de.codecentric.hc.habit.habits.Habit.ModificationRequest; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -16,17 +19,19 @@ public class HabitModuleIntegrationTest { @Autowired private HabitController habitController; + @Autowired private HabitRepository habitRepository; + + @AfterEach + void tearDown() { + habitRepository.deleteAll(); + } @Test void shouldPublishHabitCreatedEventWhenHabitIsCreated(Scenario scenario) { - Habit.ModificationRequest createJoggingHabitRequest = - Habit.ModificationRequest.builder() + ModificationRequest createJoggingHabitRequest = + ModificationRequest.builder() .name("Jogging") - .schedule( - Habit.Schedule.builder() - .frequency(Habit.Schedule.Frequency.WEEKLY) - .repetitions(3) - .build()) + .schedule(Habit.Schedule.builder().frequency(WEEKLY).repetitions(3).build()) .build(); scenario @@ -35,8 +40,28 @@ void shouldPublishHabitCreatedEventWhenHabitIsCreated(Scenario scenario) { .toArriveAndVerify( event -> { assertThat(event.name()).isEqualTo("Jogging"); - assertThat(event.frequency()).isEqualTo(Habit.Schedule.Frequency.WEEKLY); + assertThat(event.frequency()).isEqualTo(WEEKLY); assertThat(event.repetitions()).isEqualTo(3); }); } + + @Test + void shouldPublishHabitDeletedEventWhenHabitIsDeleted(Scenario scenario) { + Habit habit = + Habit.from( + ModificationRequest.builder() + .name("Jogging") + .schedule(Habit.Schedule.builder().frequency(WEEKLY).repetitions(3).build()) + .build(), + "userId"); + habitRepository.save(habit); + + scenario + .stimulate(() -> habitController.deleteHabit(habit.getId().toString(), "userId")) + .andWaitForEventOfType(Habit.HabitDeleted.class) + .toArriveAndVerify( + event -> { + assertThat(event.habitId()).isEqualTo(habit.getId()); + }); + } } diff --git a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java index 4238730b..9aa0eecd 100644 --- a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java +++ b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java @@ -72,6 +72,11 @@ public enum Frequency { } } + public Habit delete() { + registerEvent(new HabitDeleted(id)); + return this; + } + public static Habit from(ModificationRequest modificationRequest, String userId) { Habit habit = builder() @@ -82,7 +87,11 @@ public static Habit from(ModificationRequest modificationRequest, String userId) .build(); habit.registerEvent( new Habit.HabitCreated( - habit.id, habit.name, habit.schedule.frequency, habit.schedule.repetitions)); + habit.id, + habit.userId, + habit.name, + habit.schedule.frequency, + habit.schedule.repetitions)); return habit; } @@ -108,9 +117,11 @@ public static class ModificationRequest { } public record HabitCreated( - UUID habitId, String name, Schedule.Frequency frequency, Integer repetitions) { + UUID habitId, String userId, String name, Schedule.Frequency frequency, Integer repetitions) { public String getId() { return habitId.toString(); } } + + public record HabitDeleted(UUID habitId) {} } diff --git a/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java b/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java index abc63b96..0740ab35 100644 --- a/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java +++ b/services/habit/src/main/java/de/codecentric/hc/habit/habits/HabitController.java @@ -47,9 +47,11 @@ public Iterable getHabits(@Parameter(hidden = true) @UserId String userId security = {@SecurityRequirement(name = "User-ID"), @SecurityRequirement(name = "basicAuth")}) @GetMapping("/habits/{id}") @ResponseBody - public Habit getHabit(@PathVariable UUID id, @Parameter(hidden = true) @UserId String userId) { + public Habit getHabit( + @PathVariable @org.hibernate.validator.constraints.UUID String id, + @Parameter(hidden = true) @UserId String userId) { return repository - .findByIdAndUserId(id, userId) + .findByIdAndUserId(UUID.fromString(id), userId) .orElseThrow( () -> new ResponseStatusException( @@ -84,12 +86,17 @@ public ResponseEntity createHabit( security = {@SecurityRequirement(name = "User-ID"), @SecurityRequirement(name = "basicAuth")}) @DeleteMapping("/habits/{id}") public ResponseEntity deleteHabit( - @PathVariable UUID id, @Parameter(hidden = true) @UserId String userId) { - Long deletedRecords = repository.deleteByIdAndUserId(id, userId); - if (deletedRecords < 1) { - throw new ResponseStatusException( - NOT_FOUND, String.format("Habit '%s' could not be found.", id)); - } + @PathVariable @org.hibernate.validator.constraints.UUID String id, + @Parameter(hidden = true) @UserId String userId) { + var habit = repository.findByIdAndUserId(UUID.fromString(id), userId); + + habit.ifPresentOrElse( + it -> repository.delete(it.delete()), + () -> { + throw new ResponseStatusException( + NOT_FOUND, String.format("Habit '%s' could not be found.", id)); + }); + return ResponseEntity.ok().build(); } diff --git a/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java b/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java index 80d39c42..995ad3ae 100644 --- a/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java +++ b/services/habit/src/test/java/de/codecentric/hc/habit/habits/HabitControllerTest.java @@ -63,14 +63,14 @@ public void getHabitShouldReturnHabits() { public void getHabitShouldReturnHabitById() { Habit expected = DEFAULT_HABIT; when(repository.findByIdAndUserId(eq(habitId), eq(userId))).thenReturn(Optional.of(expected)); - assertThat(controller.getHabit(habitId, userId)).isEqualTo(expected); + assertThat(controller.getHabit(habitId.toString(), userId)).isEqualTo(expected); } @Test public void getHabitShouldThrowAnExceptionWhenHabitNotFound() { when(repository.findByIdAndUserId(eq(habitId), eq(userId))).thenReturn(Optional.empty()); assertThatExceptionOfType(ResponseStatusException.class) - .isThrownBy(() -> controller.getHabit(habitId, userId)) + .isThrownBy(() -> controller.getHabit(habitId.toString(), userId)) .withMessageMatching("404 NOT_FOUND \"Habit '(.*)' could not be found.\""); } @@ -135,15 +135,17 @@ public void createHabitShouldHandleUniqueHabitNameConstraintViolationException() @Test public void deleteHabitShouldDeleteHabit() { - when(repository.deleteByIdAndUserId(eq(habitId), eq(userId))).thenReturn(1l); - assertThat(controller.deleteHabit(habitId, userId)).isEqualTo(ResponseEntity.ok().build()); + when(repository.findByIdAndUserId(eq(habitId), eq(userId))) + .thenReturn(Optional.of(DEFAULT_HABIT)); + assertThat(controller.deleteHabit(habitId.toString(), userId)) + .isEqualTo(ResponseEntity.ok().build()); } @Test public void deleteHabitShouldThrowExceptionWhenHabitNotFound() { when(repository.deleteByIdAndUserId(eq(habitId), eq(userId))).thenReturn(0l); assertThatExceptionOfType(ResponseStatusException.class) - .isThrownBy(() -> controller.deleteHabit(habitId, userId)) + .isThrownBy(() -> controller.deleteHabit(habitId.toString(), userId)) .withMessageMatching("404 NOT_FOUND \"Habit '(.*)' could not be found.\""); } } From 5195622fb9a929fa3652acb55cab41fa61ee3e2a Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Wed, 3 Jan 2024 14:50:18 +0100 Subject: [PATCH 5/6] chore(habit): add delete request to sample requests --- services/habit/habit-requests.http | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/habit/habit-requests.http b/services/habit/habit-requests.http index 9832e56b..9d71f55d 100644 --- a/services/habit/habit-requests.http +++ b/services/habit/habit-requests.http @@ -16,3 +16,8 @@ Content-Type: application/json "frequency": "MONTHLY" } } + +### Delete habit +# Retrieve the ID by fetching the habits first +DELETE http://localhost:9001/habits/8ffa23a1-e91b-45a3-9060-3b0f53c0c6f3 +X-User-Id: default From cf56aa98cb9f72a4db86baf02c7cd17a3e5f6968 Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Wed, 3 Jan 2024 14:51:14 +0100 Subject: [PATCH 6/6] chore(habit): add kafka event externalization --- infrastructure/docker/docker-compose.yml | 5 ++-- .../istio/config/23-mtls-authz-policies.yaml | 1 + .../values/habit-values.yaml.gotmpl | 4 ++- .../values/kafka-values.yaml.gotmpl | 2 ++ services/habit/docker-compose.yml | 28 +++++++++++++++++++ .../de/codecentric/hc/habit/habits/Habit.java | 13 +++++---- .../habit/src/main/resources/application.yml | 11 ++++++++ 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/infrastructure/docker/docker-compose.yml b/infrastructure/docker/docker-compose.yml index 42bc1fea..699d8325 100644 --- a/infrastructure/docker/docker-compose.yml +++ b/infrastructure/docker/docker-compose.yml @@ -71,6 +71,7 @@ services: - habit-db environment: DB_HOST: habit-db + KAFKA_BOOTSTRAP_SERVERS: kafka:11001 networks: - habitcentric-net habit-db: @@ -124,8 +125,8 @@ services: - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:11001,EXTERNAL://localhost:11003 - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:11002 - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - - KAFKA_CLIENT_USERS=track,kafka-ui - - KAFKA_CLIENT_PASSWORDS=track,kafka-ui + - KAFKA_CLIENT_USERS=habit,track,kafka-ui + - KAFKA_CLIENT_PASSWORDS=habit,track,kafka-ui networks: - habitcentric-net kafka-ui: diff --git a/infrastructure/istio/config/23-mtls-authz-policies.yaml b/infrastructure/istio/config/23-mtls-authz-policies.yaml index 9c85bb02..a7706072 100644 --- a/infrastructure/istio/config/23-mtls-authz-policies.yaml +++ b/infrastructure/istio/config/23-mtls-authz-policies.yaml @@ -64,6 +64,7 @@ spec: - from: - source: principals: + - cluster.local/ns/hc-habit/sa/habit - cluster.local/ns/hc-track/sa/track --- apiVersion: security.istio.io/v1beta1 diff --git a/infrastructure/kubernetes/helmfile.d/values/habit-values.yaml.gotmpl b/infrastructure/kubernetes/helmfile.d/values/habit-values.yaml.gotmpl index 624706c5..0bb9729f 100644 --- a/infrastructure/kubernetes/helmfile.d/values/habit-values.yaml.gotmpl +++ b/infrastructure/kubernetes/helmfile.d/values/habit-values.yaml.gotmpl @@ -18,8 +18,10 @@ image: ## habitcentric habit service configuration -{{- if (eq .Environment.Name "traefik-mesh") }} extraEnv: + - name: KAFKA_BOOTSTRAP_SERVERS + value: kafka.hc-kafka.svc.cluster.local:9092 +{{- if (eq .Environment.Name "traefik-mesh") }} - name: MANAGEMENT_ZIPKIN_TRACING_ENDPOINT value: http://jaeger-collector.traefik-mesh.svc.cluster.local:9411 {{- end }} diff --git a/infrastructure/kubernetes/helmfile.d/values/kafka-values.yaml.gotmpl b/infrastructure/kubernetes/helmfile.d/values/kafka-values.yaml.gotmpl index 3834ccdb..bbf8bb82 100644 --- a/infrastructure/kubernetes/helmfile.d/values/kafka-values.yaml.gotmpl +++ b/infrastructure/kubernetes/helmfile.d/values/kafka-values.yaml.gotmpl @@ -10,8 +10,10 @@ sasl: password: "kafka" client: users: + - "habit" - "track" passwords: + - "habit" - "track" externalAccess: diff --git a/services/habit/docker-compose.yml b/services/habit/docker-compose.yml index 61428a4a..11ba1522 100644 --- a/services/habit/docker-compose.yml +++ b/services/habit/docker-compose.yml @@ -8,3 +8,31 @@ services: environment: POSTGRESQL_PASSWORD: postgres POSTGRESQL_PORT_NUMBER: 10001 + + kafka: + image: 'bitnami/kafka:latest' + ports: + - "11003:11003" + environment: + - KAFKA_CFG_NODE_ID=0 + - KAFKA_CFG_PROCESS_ROLES=controller,broker + - KAFKA_CFG_LISTENERS=PLAINTEXT://:11001,CONTROLLER://:11002,EXTERNAL://:11003 + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:SASL_PLAINTEXT,EXTERNAL:SASL_PLAINTEXT + - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:11001,EXTERNAL://localhost:11003 + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:11002 + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + - KAFKA_CLIENT_USERS=habit,kafka-ui + - KAFKA_CLIENT_PASSWORDS=habit,kafka-ui + + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - "11004:11004" + environment: + SERVER_PORT: 11004 + DYNAMIC_CONFIG_ENABLED: 'true' + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:11001 + KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SASL_PLAINTEXT + KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM: PLAIN + KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG: 'org.apache.kafka.common.security.plain.PlainLoginModule required username="kafka-ui" password="kafka-ui";' diff --git a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java index 9aa0eecd..7df6b058 100644 --- a/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java +++ b/services/habit/src/main/java/de/codecentric/hc/habit/habits/Habit.java @@ -21,6 +21,7 @@ import lombok.Setter; import lombok.ToString; import org.springframework.data.domain.AbstractAggregateRoot; +import org.springframework.modulith.events.Externalized; @Entity @Builder @@ -116,12 +117,14 @@ public static class ModificationRequest { @NotNull @Valid private Schedule schedule; } + @Externalized("habit-events::#{#this.habitId}") public record HabitCreated( - UUID habitId, String userId, String name, Schedule.Frequency frequency, Integer repetitions) { - public String getId() { - return habitId.toString(); - } - } + UUID habitId, + String userId, + String name, + Schedule.Frequency frequency, + Integer repetitions) {} + @Externalized("habit-events::#{#this.habitId}") public record HabitDeleted(UUID habitId) {} } diff --git a/services/habit/src/main/resources/application.yml b/services/habit/src/main/resources/application.yml index d1a7d91f..982c0374 100644 --- a/services/habit/src/main/resources/application.yml +++ b/services/habit/src/main/resources/application.yml @@ -24,6 +24,17 @@ spring: liquibase: change-log: db/changelog/db.changelog-master.yaml default-schema: hc_habit + + kafka: + client-id: habit + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:11003} + properties: + "security.protocol": SASL_PLAINTEXT + "sasl.mechanism": PLAIN + "sasl.jaas.config": 'org.apache.kafka.common.security.plain.PlainLoginModule required username="${KAFKA_USER:habit}" password="${KAFKA_PASSWORD:habit}";' + producer: + properties: + "spring.json.type.mapping": "habit-created:de.codecentric.hc.habit.habits.Habit.HabitCreated,habit-deleted:de.codecentric.hc.habit.habits.Habit.HabitDeleted" logging: level: liquibase.executor: WARN