From 7c75de0b2d0469ef2c0e155c9171f2bab9f2ddac Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:48:42 +0530 Subject: [PATCH 01/27] refactor(entity): replace lombok required args constructor - Replace `@RequiredArgsConstructor` with `@NoArgsConstructor` and `@AllArgsConstructor` in entity classes. - Provides a no-argument constructor for JPA compatibility and an all-argument constructor. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../eggy03/papertrail/api/entity/AuditLogRegistration.java | 6 ++++-- .../eggy03/papertrail/api/entity/MessageLogContent.java | 6 ++++-- .../papertrail/api/entity/MessageLogRegistration.java | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/github/eggy03/papertrail/api/entity/AuditLogRegistration.java b/src/main/java/io/github/eggy03/papertrail/api/entity/AuditLogRegistration.java index 1ae9c3e..c7d1ede 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/entity/AuditLogRegistration.java +++ b/src/main/java/io/github/eggy03/papertrail/api/entity/AuditLogRegistration.java @@ -4,8 +4,9 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.RequiredArgsConstructor; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @@ -13,7 +14,8 @@ @Getter @Setter @ToString -@RequiredArgsConstructor +@NoArgsConstructor +@AllArgsConstructor @Table(name = "audit_log_table") public class AuditLogRegistration { diff --git a/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogContent.java b/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogContent.java index ff5229b..222a93d 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogContent.java +++ b/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogContent.java @@ -4,8 +4,9 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.RequiredArgsConstructor; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.CreationTimestamp; @@ -16,7 +17,8 @@ @Getter @Setter @ToString -@RequiredArgsConstructor +@NoArgsConstructor +@AllArgsConstructor @Table(name = "message_log_content_table") public class MessageLogContent { diff --git a/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogRegistration.java b/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogRegistration.java index 56ddfb5..690e314 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogRegistration.java +++ b/src/main/java/io/github/eggy03/papertrail/api/entity/MessageLogRegistration.java @@ -4,8 +4,9 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.RequiredArgsConstructor; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @@ -13,7 +14,8 @@ @Getter @Setter @ToString -@RequiredArgsConstructor +@NoArgsConstructor +@AllArgsConstructor @Table(name = "message_log_registration_table") public class MessageLogRegistration { From 9e729cf258f87a289f655512f711d79baa33a408 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:49:12 +0530 Subject: [PATCH 02/27] build(test): set up test environment and add assertj - Add `assertj-core` dependency (`3.27.7`) for fluent assertions in tests - Create `src/test/resources/application.properties` for test-specific configurations - Configure Quarkus dev services for PostgreSQL (`postgres:18.2-alpine`) and Redis (`redis:8.6.1-alpine`) in tests - Set up Hibernate ORM and Flyway for test database schema management - Explicitly set `rest-assured` dependency scope to `test` Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- pom.xml | 7 +++++++ src/test/resources/application.properties | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/test/resources/application.properties diff --git a/pom.xml b/pom.xml index 9ad7531..c0b25cc 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ 3.20.0 1.18.42 4.2.0 + 3.27.7 @@ -146,6 +147,12 @@ rest-assured test + + org.assertj + assertj-core + ${assertj.core.version} + test + diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..7987dc8 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,17 @@ +quarkus.http.test-port=0 +# DB +quarkus.datasource.db-kind=postgresql +quarkus.datasource.devservices.enabled=true +quarkus.datasource.devservices.image-name=postgres:18.2-alpine +quarkus.hibernate-orm.database.default-schema=papertrailbot +quarkus.hibernate-orm.schema-management.strategy=validate +# Flyway +quarkus.flyway.migrate-at-start=true +quarkus.flyway.create-schemas=true +quarkus.flyway.schemas=papertrailbot +quarkus.flyway.default-schema=papertrailbot +# Redis +quarkus.redis.devservices.enabled=true +quarkus.redis.devservices.image-name=redis:8.6.1-alpine +# Logging Level +quarkus.log.level=DEBUG \ No newline at end of file From fe0d2be425cd4a12aa8ab27774d5248c83359556 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:49:44 +0530 Subject: [PATCH 03/27] test(audit-log-registration): add integration tests for audit log registration API - Add comprehensive integration tests for the `AuditLogRegistration` API. - Verify `POST`, `GET`, `PUT`, `DELETE` endpoints. - Cover success, conflict, not found, and bad request scenarios. - Validate input for null, malformed, and invalid parameters. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../integration/AuditLogRegistrationTest.java | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 src/test/java/integration/AuditLogRegistrationTest.java diff --git a/src/test/java/integration/AuditLogRegistrationTest.java b/src/test/java/integration/AuditLogRegistrationTest.java new file mode 100644 index 0000000..35f37a9 --- /dev/null +++ b/src/test/java/integration/AuditLogRegistrationTest.java @@ -0,0 +1,400 @@ +package integration; + +import io.github.eggy03.papertrail.api.dto.AuditLogRegistrationDTO; +import io.github.eggy03.papertrail.api.entity.AuditLogRegistration; +import io.github.eggy03.papertrail.api.repository.AuditLogRegistrationRepository; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +@QuarkusTest +class AuditLogRegistrationTest { + + private static final String BASE_PATH = "/api/v1/log/audit"; + + @Inject + AuditLogRegistrationRepository repository; + + // RedisDataSource while not annotated for CDI, does get injected because Quarkus handles this synthetic bean + // Or IntelliJ does not see the dependencies + // see https://github.com/quarkiverse/quarkus-minio/issues/413 and https://github.com/quarkusio/quarkus/discussions/25120 + @Inject + RedisDataSource redisDataSource; + + @BeforeEach + void cleanState() { + QuarkusTransaction.requiringNew().run(repository::deleteAll); + redisDataSource.flushall(); + } + + @Test + void registerGuild_success() { + + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(201) + .body("guildId", is(123)) + .body("channelId", is(456)); + + // assert that save was a success + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isPresent() + .get() + .extracting(AuditLogRegistration::getGuildId, AuditLogRegistration::getChannelId) + .containsExactly(123L, 456L); + } + + @Test + void registerGuild_exists_conflicts() { + + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(456L); + + // register once, expect success + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(201) + .body("guildId", is(123)) + .body("channelId", is(456)); + + // register again, expect 409 conflict + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(409); + + } + + @Test + void registerGuild_nullBody_badRequest() { + + // null guild and channel id + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // positive guild id but null channel id + dto.setGuildId(123L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // null guild id but positive channel id + dto.setGuildId(null); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // assert that nothing was saved + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + } + + @Test + void registerGuild_malformedBody_badRequest() { + + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + // negative guild id and channel id + dto.setGuildId(-123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // positive guild id but negative channel id + dto.setGuildId(123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // negative guild id but positive channel id + dto.setGuildId(-123L); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // invalid json + given().contentType("application/json").body("\"text\"") + .when().post(BASE_PATH) + .then().statusCode(400); + + // assert that nothing was saved + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(-123L)); + + assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); + + } + + @Test + void getGuild_success() { + + // register + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new AuditLogRegistration(123L, 456L))); + + // view registered guild - expect success + given().contentType("application/json") + .when().get(BASE_PATH + "/123") + .then().statusCode(200) + .body("guildId", is(123)) + .body("channelId", is(456)); + + } + + @Test + void getGuild_notRegistered_notFound() { + + // view un-registered guild - expect not found + given().contentType("application/json") + .when().get(BASE_PATH + "/123") + .then().statusCode(404); + + } + + @Test + void getGuild_notALong_notFound() { + + // view un-registered guild - expect not found + given().contentType("application/json") + .when().get(BASE_PATH + "/notALong") + .then().statusCode(404); + + } + + @Test + void getGuild_invalidParameter_badRequest() { + + // negative long + given().contentType("application/json") + .when().get(BASE_PATH + "/-123") + .then().statusCode(400); + + } + + @Test + void updateGuild_success() { + + // register + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new AuditLogRegistration(123L, 456L))); + + // update + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(457L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(200) + .body("guildId", is(123)) + .body("channelId", is(457)); + + // verify update + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isPresent() + .get() + .extracting(AuditLogRegistration::getGuildId, AuditLogRegistration::getChannelId) + .containsExactly(123L, 457L); + + } + + @Test + void updateGuild_doesNotExist() { + + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(456L); + + // update without registering + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(404); + + // verify update didn't register a new guild + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + + } + + @Test + void updateGuild_nullBody_badRequest() { + + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + + // null guild and channel id + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // null guild id + dto.setChannelId(456L); + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // null channel id + dto.setGuildId(123L); + dto.setChannelId(null); + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + + // verify that updates were not applied + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional).isEmpty(); + } + + @Test + void updateGuild_malformedBody_badRequest() { + + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); + // negative guild id and channel id + dto.setGuildId(-123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // positive guild id but negative channel id + dto.setGuildId(123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // negative guild id but positive channel id + dto.setGuildId(-123L); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // invalid json + given().contentType("application/json").body("\"text\"") + .when().put(BASE_PATH) + .then().statusCode(400); + + // assert that nothing was updated + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(-123L)); + + assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); + } + + @Test + void deleteGuild_success() { + + // register + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new AuditLogRegistration(123L, 456L))); + + // delete + given().contentType("application/json") + .when().delete(BASE_PATH + "/123") + .then().statusCode(204); + + // verify deletion + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + + } + + @Test + void deleteGuild_doesNotExist_notFound() { + + // attempt delete + given().contentType("application/json") + .when().delete(BASE_PATH + "/123") + .then().statusCode(404); + + // verify guild actually does not exist + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + + } + + @Test + void deleteGuild_notALong_notFound() { + + // view un-registered guild - expect not found + given().contentType("application/json") + .when().delete(BASE_PATH + "/notALong") + .then().statusCode(404); + + } + + @Test + void deleteGuild_invalidParameter_badRequest() { + + // negative long + given().contentType("application/json") + .when().delete(BASE_PATH + "/-123") + .then().statusCode(400); + + } + +} From 8e0f84edb3b706a7d566f528d704d42aaba2481d Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 09:48:34 +0530 Subject: [PATCH 04/27] test(message-log-registration): add integration tests for message log registration - add comprehensive integration tests for the `MessageLogRegistration` API - verify `POST`, `GET`, `PUT`, and `DELETE` endpoints - cover success, conflict, not found, and bad request scenarios Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../MessageLogRegistrationTest.java | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 src/test/java/integration/MessageLogRegistrationTest.java diff --git a/src/test/java/integration/MessageLogRegistrationTest.java b/src/test/java/integration/MessageLogRegistrationTest.java new file mode 100644 index 0000000..d12832d --- /dev/null +++ b/src/test/java/integration/MessageLogRegistrationTest.java @@ -0,0 +1,400 @@ +package integration; + +import io.github.eggy03.papertrail.api.dto.MessageLogRegistrationDTO; +import io.github.eggy03.papertrail.api.entity.MessageLogRegistration; +import io.github.eggy03.papertrail.api.repository.MessageLogRegistrationRepository; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +@QuarkusTest +class MessageLogRegistrationTest { + + private static final String BASE_PATH = "/api/v1/log/message"; + + @Inject + MessageLogRegistrationRepository repository; + + // RedisDataSource while not annotated for CDI, does get injected because Quarkus handles this synthetic bean + // Or IntelliJ does not see the dependencies + // see https://github.com/quarkiverse/quarkus-minio/issues/413 and https://github.com/quarkusio/quarkus/discussions/25120 + @Inject + RedisDataSource redisDataSource; + + @BeforeEach + void cleanState() { + QuarkusTransaction.requiringNew().run(repository::deleteAll); + redisDataSource.flushall(); + } + + @Test + void registerGuild_success() { + + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(201) + .body("guildId", is(123)) + .body("channelId", is(456)); + + // assert that save was a success + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isPresent() + .get() + .extracting(MessageLogRegistration::getGuildId, MessageLogRegistration::getChannelId) + .containsExactly(123L, 456L); + } + + @Test + void registerGuild_exists_conflicts() { + + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(456L); + + // register once, expect success + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(201) + .body("guildId", is(123)) + .body("channelId", is(456)); + + // register again, expect 409 conflict + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(409); + + } + + @Test + void registerGuild_nullBody_badRequest() { + + // null guild and channel id + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // positive guild id but null channel id + dto.setGuildId(123L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // null guild id but positive channel id + dto.setGuildId(null); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // assert that nothing was saved + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + } + + @Test + void registerGuild_malformedBody_badRequest() { + + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + // negative guild id and channel id + dto.setGuildId(-123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // positive guild id but negative channel id + dto.setGuildId(123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // negative guild id but positive channel id + dto.setGuildId(-123L); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // invalid json + given().contentType("application/json").body("\"text\"") + .when().post(BASE_PATH) + .then().statusCode(400); + + // assert that nothing was saved + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(-123L)); + + assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); + + } + + @Test + void getGuild_success() { + + // register + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new MessageLogRegistration(123L, 456L))); + + // view registered guild - expect success + given().contentType("application/json") + .when().get(BASE_PATH + "/123") + .then().statusCode(200) + .body("guildId", is(123)) + .body("channelId", is(456)); + + } + + @Test + void getGuild_notRegistered_notFound() { + + // view un-registered guild - expect not found + given().contentType("application/json") + .when().get(BASE_PATH + "/123") + .then().statusCode(404); + + } + + @Test + void getGuild_notALong_notFound() { + + // view un-registered guild - expect not found + given().contentType("application/json") + .when().get(BASE_PATH + "/notALong") + .then().statusCode(404); + + } + + @Test + void getGuild_invalidParameter_badRequest() { + + // negative long + given().contentType("application/json") + .when().get(BASE_PATH + "/-123") + .then().statusCode(400); + + } + + @Test + void updateGuild_success() { + + // register + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new MessageLogRegistration(123L, 456L))); + + // update + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(457L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(200) + .body("guildId", is(123)) + .body("channelId", is(457)); + + // verify update + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isPresent() + .get() + .extracting(MessageLogRegistration::getGuildId, MessageLogRegistration::getChannelId) + .containsExactly(123L, 457L); + + } + + @Test + void updateGuild_doesNotExist() { + + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + dto.setGuildId(123L); + dto.setChannelId(456L); + + // update without registering + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(404); + + // verify update didn't register a new guild + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + + } + + @Test + void updateGuild_nullBody_badRequest() { + + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + + // null guild and channel id + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // null guild id + dto.setChannelId(456L); + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // null channel id + dto.setGuildId(123L); + dto.setChannelId(null); + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + + // verify that updates were not applied + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional).isEmpty(); + } + + @Test + void updateGuild_malformedBody_badRequest() { + + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); + // negative guild id and channel id + dto.setGuildId(-123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // positive guild id but negative channel id + dto.setGuildId(123L); + dto.setChannelId(-456L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // negative guild id but positive channel id + dto.setGuildId(-123L); + dto.setChannelId(456L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // invalid json + given().contentType("application/json").body("\"text\"") + .when().put(BASE_PATH) + .then().statusCode(400); + + // assert that nothing was updated + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(-123L)); + + assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); + } + + @Test + void deleteGuild_success() { + + // register + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new MessageLogRegistration(123L, 456L))); + + // delete + given().contentType("application/json") + .when().delete(BASE_PATH + "/123") + .then().statusCode(204); + + // verify deletion + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + + } + + @Test + void deleteGuild_doesNotExist_notFound() { + + // attempt delete + given().contentType("application/json") + .when().delete(BASE_PATH + "/123") + .then().statusCode(404); + + // verify guild actually does not exist + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isEmpty(); + + } + + @Test + void deleteGuild_notALong_notFound() { + + // view un-registered guild - expect not found + given().contentType("application/json") + .when().delete(BASE_PATH + "/notALong") + .then().statusCode(404); + + } + + @Test + void deleteGuild_invalidParameter_badRequest() { + + // negative long + given().contentType("application/json") + .when().delete(BASE_PATH + "/-123") + .then().statusCode(400); + + } + +} From 2bbff2736e670c08bc4679ce79cbe03566a85b70 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 10:46:00 +0530 Subject: [PATCH 05/27] refactor(dto): add lombok constructor annotations - Add `@NoArgsConstructor` and `@AllArgsConstructor` to `MessageLogContentDTO`. - Add `@NoArgsConstructor` and `@AllArgsConstructor` to `MessageLogRegistrationDTO`. - Add `@NoArgsConstructor` and `@AllArgsConstructor` to `AuditLogRegistrationDTO`. - Provide explicit constructors for DTOs to improve object creation flexibility. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../eggy03/papertrail/api/dto/AuditLogRegistrationDTO.java | 4 ++++ .../eggy03/papertrail/api/dto/MessageLogContentDTO.java | 5 ++++- .../eggy03/papertrail/api/dto/MessageLogRegistrationDTO.java | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/eggy03/papertrail/api/dto/AuditLogRegistrationDTO.java b/src/main/java/io/github/eggy03/papertrail/api/dto/AuditLogRegistrationDTO.java index a4adf84..fbad075 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/dto/AuditLogRegistrationDTO.java +++ b/src/main/java/io/github/eggy03/papertrail/api/dto/AuditLogRegistrationDTO.java @@ -2,9 +2,13 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class AuditLogRegistrationDTO { @NotNull(message = "GuildID cannot be null") diff --git a/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogContentDTO.java b/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogContentDTO.java index 78ebdc5..ddf4485 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogContentDTO.java +++ b/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogContentDTO.java @@ -3,10 +3,13 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data - +@NoArgsConstructor +@AllArgsConstructor public class MessageLogContentDTO { @NotNull(message = "MessageID cannot be null") diff --git a/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogRegistrationDTO.java b/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogRegistrationDTO.java index 60c9651..504d679 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogRegistrationDTO.java +++ b/src/main/java/io/github/eggy03/papertrail/api/dto/MessageLogRegistrationDTO.java @@ -2,9 +2,13 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class MessageLogRegistrationDTO { @NotNull(message = "GuildID cannot be null") From 6b9caded305c5877ea2219f4e2c33cd4e4b5625c Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:26:26 +0530 Subject: [PATCH 06/27] refactor(api): use smallrye NotNull annotation - Replace `jakarta.validation.constraints.NotNull` with `io.smallrye.common.constraint.NotNull` - Align `NotNull` annotation usage with SmallRye common constraints in API services Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../papertrail/api/service/AuditLogRegistrationService.java | 2 +- .../eggy03/papertrail/api/service/MessageLogContentService.java | 2 +- .../papertrail/api/service/MessageLogRegistrationService.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java b/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java index b34898e..6578b94 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java @@ -10,9 +10,9 @@ import io.quarkus.cache.CacheInvalidate; import io.quarkus.cache.CacheKey; import io.quarkus.cache.CacheResult; +import io.smallrye.common.constraint.NotNull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; -import jakarta.validation.constraints.NotNull; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java index e0caa9a..57a74c8 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java @@ -11,9 +11,9 @@ import io.quarkus.cache.CacheKey; import io.quarkus.cache.CacheResult; import io.quarkus.scheduler.Scheduled; +import io.smallrye.common.constraint.NotNull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; -import jakarta.validation.constraints.NotNull; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java index 18e8ed2..4e96068 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java @@ -10,9 +10,9 @@ import io.quarkus.cache.CacheInvalidate; import io.quarkus.cache.CacheKey; import io.quarkus.cache.CacheResult; +import io.smallrye.common.constraint.NotNull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; -import jakarta.validation.constraints.NotNull; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; From d39713f248fcb25fa1c8cf8a092db109ce3eb6a2 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:41:35 +0530 Subject: [PATCH 07/27] refactor(message-locks): introduce configurable message operations - Abstract message operations with `MessageLogContentOperation` interface. - Implement lock-enabled and lock-disabled versions. - Select implementation via `message.locks.enabled` build property. - Update controller and `application.properties` files. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../MessageLogContentController.java | 6 +-- ...ckDisabledMessageContentOperationImpl.java | 40 +++++++++++++++++++ ...ckEnabledMessageContentOperationImpl.java} | 24 ++++++----- .../locks/MessageLogContentOperation.java | 22 ++++++++++ src/main/resources/application-dev.properties | 3 ++ .../resources/application-prod.properties | 5 ++- src/test/resources/application.properties | 6 ++- 7 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 src/main/java/io/github/eggy03/papertrail/api/service/locks/LockDisabledMessageContentOperationImpl.java rename src/main/java/io/github/eggy03/papertrail/api/service/locks/{MessageLogContentLockingService.java => LockEnabledMessageContentOperationImpl.java} (80%) create mode 100644 src/main/java/io/github/eggy03/papertrail/api/service/locks/MessageLogContentOperation.java diff --git a/src/main/java/io/github/eggy03/papertrail/api/controller/MessageLogContentController.java b/src/main/java/io/github/eggy03/papertrail/api/controller/MessageLogContentController.java index 004e467..0f1331d 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/controller/MessageLogContentController.java +++ b/src/main/java/io/github/eggy03/papertrail/api/controller/MessageLogContentController.java @@ -1,7 +1,7 @@ package io.github.eggy03.papertrail.api.controller; import io.github.eggy03.papertrail.api.dto.MessageLogContentDTO; -import io.github.eggy03.papertrail.api.service.locks.MessageLogContentLockingService; +import io.github.eggy03.papertrail.api.service.locks.MessageLogContentOperation; import io.smallrye.common.annotation.RunOnVirtualThread; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -25,7 +25,7 @@ @RequiredArgsConstructor public class MessageLogContentController { - private final MessageLogContentLockingService service; // accessing the MessageLogContentService behind Redisson locks + private final MessageLogContentOperation service; @POST public Response saveMessage(@Valid MessageLogContentDTO dto) { @@ -46,7 +46,7 @@ public Response getMessage(@PathParam("messageId") @Positive @NotNull Long messa @PUT public Response updateMessage(@Valid MessageLogContentDTO dto) { return Response - .ok(service.updateMessage(dto)) + .ok(service.updateMessage(dto.getMessageId(), dto)) .build(); } diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockDisabledMessageContentOperationImpl.java b/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockDisabledMessageContentOperationImpl.java new file mode 100644 index 0000000..1e5d597 --- /dev/null +++ b/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockDisabledMessageContentOperationImpl.java @@ -0,0 +1,40 @@ +package io.github.eggy03.papertrail.api.service.locks; + +import io.github.eggy03.papertrail.api.dto.MessageLogContentDTO; +import io.github.eggy03.papertrail.api.service.MessageLogContentService; +import io.quarkus.arc.properties.IfBuildProperty; +import io.smallrye.common.constraint.NotNull; +import jakarta.enterprise.context.ApplicationScoped; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@ApplicationScoped +@IfBuildProperty(name = "message.locks.enabled", stringValue = "false", enableIfMissing = true) +@RequiredArgsConstructor +public class LockDisabledMessageContentOperationImpl implements MessageLogContentOperation { + + private final MessageLogContentService delegate; + + @Override + @NotNull + public MessageLogContentDTO saveMessage(@NonNull MessageLogContentDTO dto) { + return delegate.saveMessage(dto); + } + + @Override + @NotNull + public MessageLogContentDTO getMessage(@NonNull Long messageId) { + return delegate.getMessage(messageId); + } + + @Override + @NotNull + public MessageLogContentDTO updateMessage(@NonNull Long messageId, @NonNull MessageLogContentDTO dto) { + return delegate.updateMessage(messageId, dto); + } + + @Override + public void deleteMessage(@NonNull Long messageId) { + delegate.deleteMessage(messageId); + } +} diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/locks/MessageLogContentLockingService.java b/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockEnabledMessageContentOperationImpl.java similarity index 80% rename from src/main/java/io/github/eggy03/papertrail/api/service/locks/MessageLogContentLockingService.java rename to src/main/java/io/github/eggy03/papertrail/api/service/locks/LockEnabledMessageContentOperationImpl.java index d8ebaae..b477162 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/locks/MessageLogContentLockingService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockEnabledMessageContentOperationImpl.java @@ -2,8 +2,9 @@ import io.github.eggy03.papertrail.api.dto.MessageLogContentDTO; import io.github.eggy03.papertrail.api.service.MessageLogContentService; +import io.quarkus.arc.properties.IfBuildProperty; +import io.smallrye.common.constraint.NotNull; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.validation.constraints.NotNull; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,29 +23,30 @@ Retry logic on update and save MAY help, but it's too much boilerplate. Kafka probably is the best solution, but I don't want to add the complexity of an entirely new system. -Usually, this should be a one-in-a-million issue unless your service health is really tanking, in which case +Usually, this should be a one-in-a-million issue unless your delegate health is really tanking, in which case maybe vertical or horizontal scaling would help more to keep up with the resource pressure */ @ApplicationScoped +@IfBuildProperty(name = "message.locks.enabled", stringValue = "true") @RequiredArgsConstructor @Slf4j -public class MessageLogContentLockingService { +public class LockEnabledMessageContentOperationImpl implements MessageLogContentOperation { private final RedissonClient redissonClient; - private final MessageLogContentService service; + private final MessageLogContentService delegate; @NotNull public MessageLogContentDTO saveMessage(@NonNull MessageLogContentDTO dto) { RLock rlock = redissonClient.getFairLock(dto.getMessageId().toString()); rlock.lock(); - log.debug("Acquired SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); + log.info("Acquired SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); try { - return service.saveMessage(dto); + return delegate.saveMessage(dto); } finally { rlock.unlock(); - log.debug("Released SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); + log.info("Released SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); } } @@ -79,7 +81,7 @@ public MessageLogContentDTO getMessage(@NonNull Long messageId) { log.debug("Acquired VIEW lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); try { - return service.getMessage(messageId); + return delegate.getMessage(messageId); } finally { rlock.unlock(); log.debug("Released VIEW lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); @@ -87,14 +89,14 @@ public MessageLogContentDTO getMessage(@NonNull Long messageId) { } @NotNull - public MessageLogContentDTO updateMessage(@NonNull MessageLogContentDTO dto) { + public MessageLogContentDTO updateMessage(@NonNull Long messageId, @NonNull MessageLogContentDTO dto) { RLock rlock = redissonClient.getFairLock(dto.getMessageId().toString()); rlock.lock(); log.debug("Acquired UPDATE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); try { - return service.updateMessage(dto.getMessageId(), dto); + return delegate.updateMessage(messageId, dto); } finally { rlock.unlock(); log.debug("Released UPDATE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); @@ -108,7 +110,7 @@ public void deleteMessage(@NonNull Long messageId) { log.debug("Acquired DELETE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); try { - service.deleteMessage(messageId); + delegate.deleteMessage(messageId); } finally { rlock.unlock(); log.debug("Released DELETE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/locks/MessageLogContentOperation.java b/src/main/java/io/github/eggy03/papertrail/api/service/locks/MessageLogContentOperation.java new file mode 100644 index 0000000..85c97d6 --- /dev/null +++ b/src/main/java/io/github/eggy03/papertrail/api/service/locks/MessageLogContentOperation.java @@ -0,0 +1,22 @@ +package io.github.eggy03.papertrail.api.service.locks; + +import io.github.eggy03.papertrail.api.dto.MessageLogContentDTO; +import lombok.NonNull; + +/** + * This interface has a signature matching {@link io.github.eggy03.papertrail.api.service.MessageLogContentService} + * Implementations of this interface will usually wrap the above described service methods in redisson locks + * or without it. + *

+ * Quarkus CDI will choose the implementation based on profiles + */ +public interface MessageLogContentOperation { + + MessageLogContentDTO saveMessage(@NonNull MessageLogContentDTO dto); + + MessageLogContentDTO getMessage(@NonNull Long messageId); + + MessageLogContentDTO updateMessage(@NonNull Long messageId, @NonNull MessageLogContentDTO dto); + + void deleteMessage(@NonNull Long messageId); +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index d3abc27..73d11b3 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -32,5 +32,8 @@ quarkus.redisson.netty-threads=32 # Logging Level quarkus.log.level=DEBUG + # Analytics quarkus.analytics.disabled=true +# Custom +message.locks.enabled=true diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 0d64d3b..cff424c 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -33,5 +33,8 @@ quarkus.http.port=${PORT:8080} # Logging Level quarkus.log.level=INFO + # Analytics -quarkus.analytics.disabled=true \ No newline at end of file +quarkus.analytics.disabled=true +# Custom +message.locks.enabled=true \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 7987dc8..f5bedf9 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -5,6 +5,7 @@ quarkus.datasource.devservices.enabled=true quarkus.datasource.devservices.image-name=postgres:18.2-alpine quarkus.hibernate-orm.database.default-schema=papertrailbot quarkus.hibernate-orm.schema-management.strategy=validate + # Flyway quarkus.flyway.migrate-at-start=true quarkus.flyway.create-schemas=true @@ -14,4 +15,7 @@ quarkus.flyway.default-schema=papertrailbot quarkus.redis.devservices.enabled=true quarkus.redis.devservices.image-name=redis:8.6.1-alpine # Logging Level -quarkus.log.level=DEBUG \ No newline at end of file +quarkus.log.level=DEBUG +# Custom +message.locks.enabled=false + From 3b2b5d8067a91eb2c489ff1c24afa29141a17650 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:42:09 +0530 Subject: [PATCH 08/27] test(message-log-content): add integration tests for `MessageLogContent` API - add integration tests for `MessageLogContent` API endpoints - cover CRUD operations: create, read, update, delete - include tests for valid requests, existing/missing resources, and invalid input Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../integration/MessageLogContentTest.java | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 src/test/java/integration/MessageLogContentTest.java diff --git a/src/test/java/integration/MessageLogContentTest.java b/src/test/java/integration/MessageLogContentTest.java new file mode 100644 index 0000000..7123540 --- /dev/null +++ b/src/test/java/integration/MessageLogContentTest.java @@ -0,0 +1,289 @@ +package integration; + +import io.github.eggy03.papertrail.api.dto.MessageLogContentDTO; +import io.github.eggy03.papertrail.api.entity.MessageLogContent; +import io.github.eggy03.papertrail.api.repository.MessageLogContentRepository; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Optional; +import java.util.stream.Stream; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; + +@QuarkusTest +class MessageLogContentTest { + + private static final String BASE_PATH = "/api/v1/content/message"; + + @Inject + MessageLogContentRepository repository; + + // RedisDataSource while not annotated for CDI, does get injected because Quarkus handles this synthetic bean + // Or IntelliJ does not see the dependencies + // see https://github.com/quarkiverse/quarkus-minio/issues/413 and https://github.com/quarkusio/quarkus/discussions/25120 + @Inject + RedisDataSource redisDataSource; + // prep a valid Entity + MessageLogContent validEntity = new MessageLogContent(123L, "message", 456L, null); + // prep a valid DTO + MessageLogContentDTO validDTO = new MessageLogContentDTO(123L, "message", 456L); + + // prep a stream of invalid DTOs + public static Stream invalidDTOs() { + + MessageLogContentDTO nullBodyDTO = new MessageLogContentDTO(null, null, null); + MessageLogContentDTO nullMessageIdDTO = new MessageLogContentDTO(null, "message", 456L); + MessageLogContentDTO nullMessageContentDTO = new MessageLogContentDTO(123L, null, 456L); + MessageLogContentDTO nullAuthorIdDTO = new MessageLogContentDTO(123L, "message", null); + + MessageLogContentDTO negativeMessageIdDTO = new MessageLogContentDTO(-123L, "message", 456L); + MessageLogContentDTO negativeAuthorIdDTO = new MessageLogContentDTO(123L, "message", -456L); + + return Stream.of(nullBodyDTO, nullMessageIdDTO, nullMessageContentDTO, nullAuthorIdDTO, negativeMessageIdDTO, negativeAuthorIdDTO); + } + + @BeforeEach + void cleanState() { + QuarkusTransaction.requiringNew().run(repository::deleteAll); + redisDataSource.flushall(); + } + + @Test + void saveMessage_success() { + + given().contentType("application/json").body(validDTO) + .when().post(BASE_PATH) + .then().statusCode(201) + .body("messageId", is(123)) + .body("messageContent", is("message")) + .body("authorId", is(456)); + + // assert that save was a success + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isPresent() + .get() + .extracting(MessageLogContent::getMessageId, MessageLogContent::getMessageContent, MessageLogContent::getAuthorId) + .containsExactly(123L, "message", 456L); + } + + @Test + void saveMessage_exists_conflicts() { + + // save once, expect success + given().contentType("application/json").body(validDTO) + .when().post(BASE_PATH) + .then().statusCode(201) + .body("messageId", is(123)) + .body("messageContent", is("message")) + .body("authorId", is(456)); + + // save again, expect 409 conflict + given().contentType("application/json").body(validDTO) + .when().post(BASE_PATH) + .then().statusCode(409); + + } + + @ParameterizedTest + @MethodSource("invalidDTOs") + void saveMessage_validationFails_badRequest(MessageLogContentDTO dto) { + + given().contentType("application/json").body(dto) + .when().post(BASE_PATH) + .then().statusCode(400); + + // assert that nothing was saved + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional).isEmpty(); + } + + @Test + void saveMessage_deserializationFails_badRequest() { + + given().contentType("application/json").body("\"text\"") + .when().post(BASE_PATH) + .then().statusCode(400); + + } + + @Test + void getMessage_success() { + + // save to db + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); + + // view message - expect success + given().contentType("application/json") + .when().get(BASE_PATH + "/123") + .then().statusCode(200) + .body("messageId", is(123)) + .body("messageContent", is("message")) + .body("authorId", is(456)); + + } + + @Test + void getMessage_notSaved_notFound() { + + given().contentType("application/json") + .when().get(BASE_PATH + "/123") + .then().statusCode(404); + + } + + @Test + void getMessage_invalidParameters() { + + given().contentType("application/json") + .when().get(BASE_PATH + "/notALong") + .then().statusCode(404); + + given().contentType("application/json") + .when().get(BASE_PATH + "/-123") + .then().statusCode(400); + + } + + @Test + void updateMessage_success() { + + // save message + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); + + // create an updated DTO + MessageLogContentDTO dto = new MessageLogContentDTO(123L, "updatedMessage", 457L); + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(200) + .body("messageId", is(123)) + .body("messageContent", is("updatedMessage")) + .body("authorId", is(457)); + + // verify update + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional) + .isPresent() + .get() + .extracting(MessageLogContent::getMessageId, MessageLogContent::getMessageContent, MessageLogContent::getAuthorId) + .containsExactly(123L, "updatedMessage", 457L); + + } + + @Test + void updateMessage_doesNotExist_notFound() { + + // update without saving first + given().contentType("application/json").body(validDTO) + .when().put(BASE_PATH) + .then().statusCode(404); + + // verify update didn't register a new guild + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional).isEmpty(); + + } + + @ParameterizedTest + @MethodSource("invalidDTOs") + void updateGuild_validationFails_badRequest(MessageLogContentDTO dto) { + + given().contentType("application/json").body(dto) + .when().put(BASE_PATH) + .then().statusCode(400); + + // verify that updates were not applied + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(-123L)); + + assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); + } + + @Test + void updateGuild_deserializationFails_badRequest() { + + given().contentType("application/json").body("\"text\"") + .when().put(BASE_PATH) + .then().statusCode(400); + + } + + @Test + void deleteMessage_success() { + + // register + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); + + // delete + given().contentType("application/json") + .when().delete(BASE_PATH + "/123") + .then().statusCode(204); + + // verify deletion + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional).isEmpty(); + + } + + @Test + void deleteMessage_doesNotExist_notFound() { + + // attempt delete + given().contentType("application/json") + .when().delete(BASE_PATH + "/123") + .then().statusCode(404); + + // verify guild actually does not exist + Optional entityOptional = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(123L)); + + assertThat(entityOptional).isEmpty(); + + } + + @Test + void deleteMessage_invalidParameters() { + + given().contentType("application/json") + .when().delete(BASE_PATH + "/notALong") + .then().statusCode(404); + + given().contentType("application/json") + .when().delete(BASE_PATH + "/-123") + .then().statusCode(400); + + } + +} From 3e8ca428ca062b2e77234fc1495e1608640edd00 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:08:45 +0530 Subject: [PATCH 09/27] refactor(locks): revert logging level for save lock operations - revert `log.info` to `log.debug` for lock acquisition and release messages which was changed temporarily to test whether the locking bean was selected correctly based on configuration. - update comment from `delegate health` to `service health` as this was a product of a mass refactor Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../locks/LockEnabledMessageContentOperationImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockEnabledMessageContentOperationImpl.java b/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockEnabledMessageContentOperationImpl.java index b477162..4b26276 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockEnabledMessageContentOperationImpl.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/locks/LockEnabledMessageContentOperationImpl.java @@ -23,7 +23,7 @@ Retry logic on update and save MAY help, but it's too much boilerplate. Kafka probably is the best solution, but I don't want to add the complexity of an entirely new system. -Usually, this should be a one-in-a-million issue unless your delegate health is really tanking, in which case +Usually, this should be a one-in-a-million issue unless your service health is really tanking, in which case maybe vertical or horizontal scaling would help more to keep up with the resource pressure */ @ApplicationScoped @@ -40,13 +40,13 @@ public MessageLogContentDTO saveMessage(@NonNull MessageLogContentDTO dto) { RLock rlock = redissonClient.getFairLock(dto.getMessageId().toString()); rlock.lock(); - log.info("Acquired SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); + log.debug("Acquired SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); try { return delegate.saveMessage(dto); } finally { rlock.unlock(); - log.info("Released SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); + log.debug("Released SAVE lock for messageID {} with active lock count {}", rlock.getName(), rlock.getHoldCount()); } } From af8ae9e02d58e2d6d59213640f16416d12519765 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 15:52:51 +0530 Subject: [PATCH 10/27] refactor(test): introduce constants for message log content test data - introduce `static final` constants for `messageId`, `messageContent`, and `authorId` values in `MessageLogContentTest.java` - replace hardcoded test data with the new constants for improved readability and maintainability - rename `saveMessage_exists_conflicts` test method to `saveMessage_alreadyExists_conflicts` - add assertion for negative `messageId` in `saveMessage_invalid_badRequest` test Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../integration/MessageLogContentTest.java | 85 +++++++++++-------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/src/test/java/integration/MessageLogContentTest.java b/src/test/java/integration/MessageLogContentTest.java index 7123540..def585d 100644 --- a/src/test/java/integration/MessageLogContentTest.java +++ b/src/test/java/integration/MessageLogContentTest.java @@ -32,21 +32,29 @@ class MessageLogContentTest { // see https://github.com/quarkiverse/quarkus-minio/issues/413 and https://github.com/quarkusio/quarkus/discussions/25120 @Inject RedisDataSource redisDataSource; + + static final Long TEST_MESSAGE_ID = 1302148573926148096L; + static final String TEST_MESSAGE_CONTENT = "message"; + static final Long TEST_AUTHOR_ID = 1302148573926148097L; + + static final Long NEGATIVE_TEST_MESSAGE_ID = -1302148573926148096L; + static final Long NEGATIVE_TEST_AUTHOR_ID = -1302148573926148097L; + // prep a valid Entity - MessageLogContent validEntity = new MessageLogContent(123L, "message", 456L, null); + final MessageLogContent validEntity = new MessageLogContent(TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, TEST_AUTHOR_ID, null); // prep a valid DTO - MessageLogContentDTO validDTO = new MessageLogContentDTO(123L, "message", 456L); + final MessageLogContentDTO validDTO = new MessageLogContentDTO(TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, TEST_AUTHOR_ID); // prep a stream of invalid DTOs public static Stream invalidDTOs() { MessageLogContentDTO nullBodyDTO = new MessageLogContentDTO(null, null, null); - MessageLogContentDTO nullMessageIdDTO = new MessageLogContentDTO(null, "message", 456L); - MessageLogContentDTO nullMessageContentDTO = new MessageLogContentDTO(123L, null, 456L); - MessageLogContentDTO nullAuthorIdDTO = new MessageLogContentDTO(123L, "message", null); + MessageLogContentDTO nullMessageIdDTO = new MessageLogContentDTO(null, TEST_MESSAGE_CONTENT, TEST_AUTHOR_ID); + MessageLogContentDTO nullMessageContentDTO = new MessageLogContentDTO(TEST_MESSAGE_ID, null, TEST_AUTHOR_ID); + MessageLogContentDTO nullAuthorIdDTO = new MessageLogContentDTO(TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, null); - MessageLogContentDTO negativeMessageIdDTO = new MessageLogContentDTO(-123L, "message", 456L); - MessageLogContentDTO negativeAuthorIdDTO = new MessageLogContentDTO(123L, "message", -456L); + MessageLogContentDTO negativeMessageIdDTO = new MessageLogContentDTO(NEGATIVE_TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, TEST_AUTHOR_ID); + MessageLogContentDTO negativeAuthorIdDTO = new MessageLogContentDTO(TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, NEGATIVE_TEST_AUTHOR_ID); return Stream.of(nullBodyDTO, nullMessageIdDTO, nullMessageContentDTO, nullAuthorIdDTO, negativeMessageIdDTO, negativeAuthorIdDTO); } @@ -63,32 +71,32 @@ void saveMessage_success() { given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(201) - .body("messageId", is(123)) - .body("messageContent", is("message")) - .body("authorId", is(456)); + .body("messageId", is(TEST_MESSAGE_ID)) + .body("messageContent", is(TEST_MESSAGE_CONTENT)) + .body("authorId", is(TEST_AUTHOR_ID)); // assert that save was a success Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_MESSAGE_ID)); assertThat(entityOptional) .isPresent() .get() .extracting(MessageLogContent::getMessageId, MessageLogContent::getMessageContent, MessageLogContent::getAuthorId) - .containsExactly(123L, "message", 456L); + .containsExactly(TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, TEST_AUTHOR_ID); } @Test - void saveMessage_exists_conflicts() { + void saveMessage_alreadyExists_conflicts() { // save once, expect success given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(201) - .body("messageId", is(123)) - .body("messageContent", is("message")) - .body("authorId", is(456)); + .body("messageId", is(TEST_MESSAGE_ID)) + .body("messageContent", is(TEST_MESSAGE_CONTENT)) + .body("authorId", is(TEST_AUTHOR_ID)); // save again, expect 409 conflict given().contentType("application/json").body(validDTO) @@ -108,9 +116,14 @@ void saveMessage_validationFails_badRequest(MessageLogContentDTO dto) { // assert that nothing was saved Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_MESSAGE_ID)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(NEGATIVE_TEST_MESSAGE_ID)); assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); } @Test @@ -130,11 +143,11 @@ void getMessage_success() { // view message - expect success given().contentType("application/json") - .when().get(BASE_PATH + "/123") + .when().get(BASE_PATH + "/" + TEST_MESSAGE_ID) .then().statusCode(200) - .body("messageId", is(123)) - .body("messageContent", is("message")) - .body("authorId", is(456)); + .body("messageId", is(TEST_MESSAGE_ID)) + .body("messageContent", is(TEST_MESSAGE_CONTENT)) + .body("authorId", is(TEST_AUTHOR_ID)); } @@ -142,7 +155,7 @@ void getMessage_success() { void getMessage_notSaved_notFound() { given().contentType("application/json") - .when().get(BASE_PATH + "/123") + .when().get(BASE_PATH + "/" + TEST_MESSAGE_ID) .then().statusCode(404); } @@ -155,7 +168,7 @@ void getMessage_invalidParameters() { .then().statusCode(404); given().contentType("application/json") - .when().get(BASE_PATH + "/-123") + .when().get(BASE_PATH + "/" + NEGATIVE_TEST_MESSAGE_ID) .then().statusCode(400); } @@ -167,25 +180,25 @@ void updateMessage_success() { QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); // create an updated DTO - MessageLogContentDTO dto = new MessageLogContentDTO(123L, "updatedMessage", 457L); + MessageLogContentDTO dto = new MessageLogContentDTO(TEST_MESSAGE_ID, "updatedMessage", TEST_AUTHOR_ID); given().contentType("application/json").body(dto) .when().put(BASE_PATH) .then().statusCode(200) - .body("messageId", is(123)) + .body("messageId", is(TEST_MESSAGE_ID)) .body("messageContent", is("updatedMessage")) - .body("authorId", is(457)); + .body("authorId", is(TEST_AUTHOR_ID)); // verify update Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_MESSAGE_ID)); assertThat(entityOptional) .isPresent() .get() .extracting(MessageLogContent::getMessageId, MessageLogContent::getMessageContent, MessageLogContent::getAuthorId) - .containsExactly(123L, "updatedMessage", 457L); + .containsExactly(TEST_MESSAGE_ID, "updatedMessage", TEST_AUTHOR_ID); } @@ -200,7 +213,7 @@ void updateMessage_doesNotExist_notFound() { // verify update didn't register a new guild Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_MESSAGE_ID)); assertThat(entityOptional).isEmpty(); @@ -217,11 +230,11 @@ void updateGuild_validationFails_badRequest(MessageLogContentDTO dto) { // verify that updates were not applied Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_MESSAGE_ID)); Optional entityOptionalTwo = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(-123L)); + .call(() -> repository.findByIdOptional(NEGATIVE_TEST_MESSAGE_ID)); assertThat(entityOptional).isEmpty(); assertThat(entityOptionalTwo).isEmpty(); @@ -244,13 +257,13 @@ void deleteMessage_success() { // delete given().contentType("application/json") - .when().delete(BASE_PATH + "/123") + .when().delete(BASE_PATH + "/" + TEST_MESSAGE_ID) .then().statusCode(204); // verify deletion Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_MESSAGE_ID)); assertThat(entityOptional).isEmpty(); @@ -261,13 +274,13 @@ void deleteMessage_doesNotExist_notFound() { // attempt delete given().contentType("application/json") - .when().delete(BASE_PATH + "/123") + .when().delete(BASE_PATH + "/" + TEST_MESSAGE_ID) .then().statusCode(404); // verify guild actually does not exist Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_MESSAGE_ID)); assertThat(entityOptional).isEmpty(); @@ -281,7 +294,7 @@ void deleteMessage_invalidParameters() { .then().statusCode(404); given().contentType("application/json") - .when().delete(BASE_PATH + "/-123") + .when().delete(BASE_PATH + "/" + NEGATIVE_TEST_MESSAGE_ID) .then().statusCode(400); } From 2323d64ed0116efcfd659657a2f4538974d21200 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:21:34 +0530 Subject: [PATCH 11/27] test(integration-tests): refactor log registration validation tests - introduce parameterized tests for invalid DTO and path parameter validation - consolidate multiple individual validation tests into single parameterized tests - define constants for test data like `TEST_GUILD_ID` and `NEGATIVE_TEST_GUILD_ID` - separate deserialization failure tests from validation failure tests Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../integration/AuditLogRegistrationTest.java | 272 ++++++------------ .../MessageLogRegistrationTest.java | 272 ++++++------------ 2 files changed, 168 insertions(+), 376 deletions(-) diff --git a/src/test/java/integration/AuditLogRegistrationTest.java b/src/test/java/integration/AuditLogRegistrationTest.java index 35f37a9..d5cda19 100644 --- a/src/test/java/integration/AuditLogRegistrationTest.java +++ b/src/test/java/integration/AuditLogRegistrationTest.java @@ -9,8 +9,11 @@ import jakarta.inject.Inject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.Optional; +import java.util.stream.Stream; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; @@ -30,6 +33,30 @@ class AuditLogRegistrationTest { @Inject RedisDataSource redisDataSource; + static final Long TEST_GUILD_ID = 1302148573926148096L; + static final Long TEST_CHANNEL_ID = 1302148573926148097L; + + static final Long NEGATIVE_TEST_GUILD_ID = -1302148573926148096L; + static final Long NEGATIVE_TEST_CHANNEL_ID = -1302148573926148097L; + + // prep a valid Entity + final AuditLogRegistration validEntity = new AuditLogRegistration(TEST_GUILD_ID, TEST_CHANNEL_ID); + // prep a valid DTO + final AuditLogRegistrationDTO validDTO = new AuditLogRegistrationDTO(TEST_GUILD_ID, TEST_CHANNEL_ID); + + // prep a stream of invalid DTOs + public static Stream invalidDTOs() { + + AuditLogRegistrationDTO nullBodyDTO = new AuditLogRegistrationDTO(null, null); + AuditLogRegistrationDTO nullGuildIdDTO = new AuditLogRegistrationDTO(null, TEST_CHANNEL_ID); + AuditLogRegistrationDTO nullChannelIdDTO = new AuditLogRegistrationDTO(TEST_GUILD_ID, null); + + AuditLogRegistrationDTO negativeGuildIdDTO = new AuditLogRegistrationDTO(NEGATIVE_TEST_GUILD_ID, TEST_CHANNEL_ID); + AuditLogRegistrationDTO negativeChannelIdDTO = new AuditLogRegistrationDTO(TEST_GUILD_ID, NEGATIVE_TEST_CHANNEL_ID); + + return Stream.of(nullBodyDTO, nullGuildIdDTO, nullChannelIdDTO, negativeGuildIdDTO, negativeChannelIdDTO); + } + @BeforeEach void cleanState() { QuarkusTransaction.requiringNew().run(repository::deleteAll); @@ -39,69 +66,44 @@ void cleanState() { @Test void registerGuild_success() { - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(456L); - - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(201) - .body("guildId", is(123)) - .body("channelId", is(456)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(TEST_CHANNEL_ID)); // assert that save was a success Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); assertThat(entityOptional) .isPresent() .get() .extracting(AuditLogRegistration::getGuildId, AuditLogRegistration::getChannelId) - .containsExactly(123L, 456L); + .containsExactly(TEST_GUILD_ID, TEST_CHANNEL_ID); } @Test - void registerGuild_exists_conflicts() { - - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(456L); + void registerGuild_alreadyExists_conflicts() { // register once, expect success - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(201) - .body("guildId", is(123)) - .body("channelId", is(456)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(TEST_CHANNEL_ID)); // register again, expect 409 conflict - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(409); } - @Test - void registerGuild_nullBody_badRequest() { - - // null guild and channel id - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // positive guild id but null channel id - dto.setGuildId(123L); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // null guild id but positive channel id - dto.setGuildId(null); - dto.setChannelId(456L); + @ParameterizedTest + @MethodSource("invalidDTOs") + void registerGuild_validationFails_badRequest(AuditLogRegistrationDTO dto) { given().contentType("application/json").body(dto) .when().post(BASE_PATH) @@ -110,71 +112,37 @@ void registerGuild_nullBody_badRequest() { // assert that nothing was saved Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(NEGATIVE_TEST_GUILD_ID)); + + assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); } @Test - void registerGuild_malformedBody_badRequest() { - - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - // negative guild id and channel id - dto.setGuildId(-123L); - dto.setChannelId(-456L); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // positive guild id but negative channel id - dto.setGuildId(123L); - dto.setChannelId(-456L); + void registerGuild_deserializationFails_badRequest() { - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // negative guild id but positive channel id - dto.setGuildId(-123L); - dto.setChannelId(456L); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // invalid json given().contentType("application/json").body("\"text\"") .when().post(BASE_PATH) .then().statusCode(400); - // assert that nothing was saved - Optional entityOptional = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(123L)); - - Optional entityOptionalTwo = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(-123L)); - - assertThat(entityOptional).isEmpty(); - assertThat(entityOptionalTwo).isEmpty(); - } @Test void getGuild_success() { // register - QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new AuditLogRegistration(123L, 456L))); + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); // view registered guild - expect success given().contentType("application/json") - .when().get(BASE_PATH + "/123") + .when().get(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(200) - .body("guildId", is(123)) - .body("channelId", is(456)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(TEST_CHANNEL_ID)); } @@ -183,27 +151,20 @@ void getGuild_notRegistered_notFound() { // view un-registered guild - expect not found given().contentType("application/json") - .when().get(BASE_PATH + "/123") + .when().get(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(404); } @Test - void getGuild_notALong_notFound() { + void getGuild_invalidParameters() { - // view un-registered guild - expect not found given().contentType("application/json") .when().get(BASE_PATH + "/notALong") .then().statusCode(404); - } - - @Test - void getGuild_invalidParameter_badRequest() { - - // negative long given().contentType("application/json") - .when().get(BASE_PATH + "/-123") + .when().get(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) .then().statusCode(400); } @@ -212,150 +173,94 @@ void getGuild_invalidParameter_badRequest() { void updateGuild_success() { // register - QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new AuditLogRegistration(123L, 456L))); + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); // update - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(457L); + AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(TEST_GUILD_ID, 1302148579426154496L); given().contentType("application/json").body(dto) .when().put(BASE_PATH) .then().statusCode(200) - .body("guildId", is(123)) - .body("channelId", is(457)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(1302148579426154496L)); // verify update Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); assertThat(entityOptional) .isPresent() .get() .extracting(AuditLogRegistration::getGuildId, AuditLogRegistration::getChannelId) - .containsExactly(123L, 457L); + .containsExactly(TEST_GUILD_ID, 1302148579426154496L); } @Test void updateGuild_doesNotExist() { - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(456L); - // update without registering - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().put(BASE_PATH) .then().statusCode(404); // verify update didn't register a new guild Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + assertThat(entityOptional).isEmpty(); } - @Test - void updateGuild_nullBody_badRequest() { + @ParameterizedTest + @MethodSource("invalidDTOs") + void updateGuild_validationFails_badRequest(AuditLogRegistrationDTO dto) { - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - - // null guild and channel id given().contentType("application/json").body(dto) .when().put(BASE_PATH) .then().statusCode(400); - // null guild id - dto.setChannelId(456L); - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // null channel id - dto.setGuildId(123L); - dto.setChannelId(null); - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - - // verify that updates were not applied + // assert that nothing was updated Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(NEGATIVE_TEST_GUILD_ID)); assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); } @Test - void updateGuild_malformedBody_badRequest() { - - AuditLogRegistrationDTO dto = new AuditLogRegistrationDTO(); - // negative guild id and channel id - dto.setGuildId(-123L); - dto.setChannelId(-456L); - - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // positive guild id but negative channel id - dto.setGuildId(123L); - dto.setChannelId(-456L); + void updateGuild_deserializationFails_badRequest() { - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // negative guild id but positive channel id - dto.setGuildId(-123L); - dto.setChannelId(456L); - - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // invalid json given().contentType("application/json").body("\"text\"") .when().put(BASE_PATH) .then().statusCode(400); - // assert that nothing was updated - Optional entityOptional = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(123L)); - - Optional entityOptionalTwo = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(-123L)); - - assertThat(entityOptional).isEmpty(); - assertThat(entityOptionalTwo).isEmpty(); } @Test void deleteGuild_success() { // register - QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new AuditLogRegistration(123L, 456L))); + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); // delete given().contentType("application/json") - .when().delete(BASE_PATH + "/123") + .when().delete(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(204); // verify deletion Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + assertThat(entityOptional).isEmpty(); } @@ -364,35 +269,26 @@ void deleteGuild_doesNotExist_notFound() { // attempt delete given().contentType("application/json") - .when().delete(BASE_PATH + "/123") + .when().delete(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(404); // verify guild actually does not exist Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + assertThat(entityOptional).isEmpty(); } @Test - void deleteGuild_notALong_notFound() { - - // view un-registered guild - expect not found + void deleteGuild_invalidParameters() { given().contentType("application/json") .when().delete(BASE_PATH + "/notALong") .then().statusCode(404); - } - - @Test - void deleteGuild_invalidParameter_badRequest() { - - // negative long given().contentType("application/json") - .when().delete(BASE_PATH + "/-123") + .when().delete(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) .then().statusCode(400); } diff --git a/src/test/java/integration/MessageLogRegistrationTest.java b/src/test/java/integration/MessageLogRegistrationTest.java index d12832d..dfe7951 100644 --- a/src/test/java/integration/MessageLogRegistrationTest.java +++ b/src/test/java/integration/MessageLogRegistrationTest.java @@ -9,8 +9,11 @@ import jakarta.inject.Inject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.Optional; +import java.util.stream.Stream; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; @@ -30,6 +33,30 @@ class MessageLogRegistrationTest { @Inject RedisDataSource redisDataSource; + static final Long TEST_GUILD_ID = 1302148573926148096L; + static final Long TEST_CHANNEL_ID = 1302148573926148097L; + + static final Long NEGATIVE_TEST_GUILD_ID = -1302148573926148096L; + static final Long NEGATIVE_TEST_CHANNEL_ID = -1302148573926148097L; + + // prep a valid Entity + final MessageLogRegistration validEntity = new MessageLogRegistration(TEST_GUILD_ID, TEST_CHANNEL_ID); + // prep a valid DTO + final MessageLogRegistrationDTO validDTO = new MessageLogRegistrationDTO(TEST_GUILD_ID, TEST_CHANNEL_ID); + + // prep a stream of invalid DTOs + public static Stream invalidDTOs() { + + MessageLogRegistrationDTO nullBodyDTO = new MessageLogRegistrationDTO(null, null); + MessageLogRegistrationDTO nullGuildIdDTO = new MessageLogRegistrationDTO(null, TEST_CHANNEL_ID); + MessageLogRegistrationDTO nullChannelIdDTO = new MessageLogRegistrationDTO(TEST_GUILD_ID, null); + + MessageLogRegistrationDTO negativeGuildIdDTO = new MessageLogRegistrationDTO(NEGATIVE_TEST_GUILD_ID, TEST_CHANNEL_ID); + MessageLogRegistrationDTO negativeChannelIdDTO = new MessageLogRegistrationDTO(TEST_GUILD_ID, NEGATIVE_TEST_CHANNEL_ID); + + return Stream.of(nullBodyDTO, nullGuildIdDTO, nullChannelIdDTO, negativeGuildIdDTO, negativeChannelIdDTO); + } + @BeforeEach void cleanState() { QuarkusTransaction.requiringNew().run(repository::deleteAll); @@ -39,69 +66,44 @@ void cleanState() { @Test void registerGuild_success() { - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(456L); - - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(201) - .body("guildId", is(123)) - .body("channelId", is(456)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(TEST_CHANNEL_ID)); // assert that save was a success Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); assertThat(entityOptional) .isPresent() .get() .extracting(MessageLogRegistration::getGuildId, MessageLogRegistration::getChannelId) - .containsExactly(123L, 456L); + .containsExactly(TEST_GUILD_ID, TEST_CHANNEL_ID); } @Test - void registerGuild_exists_conflicts() { - - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(456L); + void registerGuild_alreadyExists_conflicts() { // register once, expect success - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(201) - .body("guildId", is(123)) - .body("channelId", is(456)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(TEST_CHANNEL_ID)); // register again, expect 409 conflict - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().post(BASE_PATH) .then().statusCode(409); } - @Test - void registerGuild_nullBody_badRequest() { - - // null guild and channel id - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // positive guild id but null channel id - dto.setGuildId(123L); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // null guild id but positive channel id - dto.setGuildId(null); - dto.setChannelId(456L); + @ParameterizedTest + @MethodSource("invalidDTOs") + void registerGuild_validationFails_badRequest(MessageLogRegistrationDTO dto) { given().contentType("application/json").body(dto) .when().post(BASE_PATH) @@ -110,71 +112,37 @@ void registerGuild_nullBody_badRequest() { // assert that nothing was saved Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(NEGATIVE_TEST_GUILD_ID)); + + assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); } @Test - void registerGuild_malformedBody_badRequest() { - - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - // negative guild id and channel id - dto.setGuildId(-123L); - dto.setChannelId(-456L); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // positive guild id but negative channel id - dto.setGuildId(123L); - dto.setChannelId(-456L); + void registerGuild_deserializationFails_badRequest() { - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // negative guild id but positive channel id - dto.setGuildId(-123L); - dto.setChannelId(456L); - - given().contentType("application/json").body(dto) - .when().post(BASE_PATH) - .then().statusCode(400); - - // invalid json given().contentType("application/json").body("\"text\"") .when().post(BASE_PATH) .then().statusCode(400); - // assert that nothing was saved - Optional entityOptional = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(123L)); - - Optional entityOptionalTwo = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(-123L)); - - assertThat(entityOptional).isEmpty(); - assertThat(entityOptionalTwo).isEmpty(); - } @Test void getGuild_success() { // register - QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new MessageLogRegistration(123L, 456L))); + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); // view registered guild - expect success given().contentType("application/json") - .when().get(BASE_PATH + "/123") + .when().get(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(200) - .body("guildId", is(123)) - .body("channelId", is(456)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(TEST_CHANNEL_ID)); } @@ -183,27 +151,20 @@ void getGuild_notRegistered_notFound() { // view un-registered guild - expect not found given().contentType("application/json") - .when().get(BASE_PATH + "/123") + .when().get(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(404); } @Test - void getGuild_notALong_notFound() { + void getGuild_invalidParameters() { - // view un-registered guild - expect not found given().contentType("application/json") .when().get(BASE_PATH + "/notALong") .then().statusCode(404); - } - - @Test - void getGuild_invalidParameter_badRequest() { - - // negative long given().contentType("application/json") - .when().get(BASE_PATH + "/-123") + .when().get(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) .then().statusCode(400); } @@ -212,150 +173,94 @@ void getGuild_invalidParameter_badRequest() { void updateGuild_success() { // register - QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new MessageLogRegistration(123L, 456L))); + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); // update - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(457L); + MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(TEST_GUILD_ID, 1302148579426154496L); given().contentType("application/json").body(dto) .when().put(BASE_PATH) .then().statusCode(200) - .body("guildId", is(123)) - .body("channelId", is(457)); + .body("guildId", is(TEST_GUILD_ID)) + .body("channelId", is(1302148579426154496L)); // verify update Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); assertThat(entityOptional) .isPresent() .get() .extracting(MessageLogRegistration::getGuildId, MessageLogRegistration::getChannelId) - .containsExactly(123L, 457L); + .containsExactly(TEST_GUILD_ID, 1302148579426154496L); } @Test void updateGuild_doesNotExist() { - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - dto.setGuildId(123L); - dto.setChannelId(456L); - // update without registering - given().contentType("application/json").body(dto) + given().contentType("application/json").body(validDTO) .when().put(BASE_PATH) .then().statusCode(404); // verify update didn't register a new guild Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + assertThat(entityOptional).isEmpty(); } - @Test - void updateGuild_nullBody_badRequest() { + @ParameterizedTest + @MethodSource("invalidDTOs") + void updateGuild_validationFails_badRequest(MessageLogRegistrationDTO dto) { - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - - // null guild and channel id given().contentType("application/json").body(dto) .when().put(BASE_PATH) .then().statusCode(400); - // null guild id - dto.setChannelId(456L); - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // null channel id - dto.setGuildId(123L); - dto.setChannelId(null); - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - - // verify that updates were not applied + // assert that nothing was updated Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); + + Optional entityOptionalTwo = QuarkusTransaction + .requiringNew() + .call(() -> repository.findByIdOptional(NEGATIVE_TEST_GUILD_ID)); assertThat(entityOptional).isEmpty(); + assertThat(entityOptionalTwo).isEmpty(); } @Test - void updateGuild_malformedBody_badRequest() { - - MessageLogRegistrationDTO dto = new MessageLogRegistrationDTO(); - // negative guild id and channel id - dto.setGuildId(-123L); - dto.setChannelId(-456L); - - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // positive guild id but negative channel id - dto.setGuildId(123L); - dto.setChannelId(-456L); + void updateGuild_deserializationFails_badRequest() { - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // negative guild id but positive channel id - dto.setGuildId(-123L); - dto.setChannelId(456L); - - given().contentType("application/json").body(dto) - .when().put(BASE_PATH) - .then().statusCode(400); - - // invalid json given().contentType("application/json").body("\"text\"") .when().put(BASE_PATH) .then().statusCode(400); - // assert that nothing was updated - Optional entityOptional = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(123L)); - - Optional entityOptionalTwo = QuarkusTransaction - .requiringNew() - .call(() -> repository.findByIdOptional(-123L)); - - assertThat(entityOptional).isEmpty(); - assertThat(entityOptionalTwo).isEmpty(); } @Test void deleteGuild_success() { // register - QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(new MessageLogRegistration(123L, 456L))); + QuarkusTransaction.requiringNew().run(() -> repository.persistAndFlush(validEntity)); // delete given().contentType("application/json") - .when().delete(BASE_PATH + "/123") + .when().delete(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(204); // verify deletion Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + assertThat(entityOptional).isEmpty(); } @@ -364,35 +269,26 @@ void deleteGuild_doesNotExist_notFound() { // attempt delete given().contentType("application/json") - .when().delete(BASE_PATH + "/123") + .when().delete(BASE_PATH + "/" + TEST_GUILD_ID) .then().statusCode(404); // verify guild actually does not exist Optional entityOptional = QuarkusTransaction .requiringNew() - .call(() -> repository.findByIdOptional(123L)); + .call(() -> repository.findByIdOptional(TEST_GUILD_ID)); - assertThat(entityOptional) - .isEmpty(); + assertThat(entityOptional).isEmpty(); } @Test - void deleteGuild_notALong_notFound() { - - // view un-registered guild - expect not found + void deleteGuild_invalidParameters() { given().contentType("application/json") .when().delete(BASE_PATH + "/notALong") .then().statusCode(404); - } - - @Test - void deleteGuild_invalidParameter_badRequest() { - - // negative long given().contentType("application/json") - .when().delete(BASE_PATH + "/-123") + .when().delete(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) .then().statusCode(400); } From 2b293cbcc5a1b6b527bac315d4c4a3e4bc9e9487 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:26:38 +0530 Subject: [PATCH 12/27] test(integration): remove 404 tests for invalid long parameters - path/notALong would usually be checked for valid API paths, rather than a valid Long value. While conversion happens internally and exceptions are thrown, it is eventually wrapped in a NotFoundException by Quarkus, indicating that a resource path like that does not exist, meaning it checks for path validity before type validity, so it's mostly testing framework behavior at this point, which is why these tests have been removed. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- src/test/java/integration/AuditLogRegistrationTest.java | 7 ------- src/test/java/integration/MessageLogContentTest.java | 8 -------- src/test/java/integration/MessageLogRegistrationTest.java | 7 ------- 3 files changed, 22 deletions(-) diff --git a/src/test/java/integration/AuditLogRegistrationTest.java b/src/test/java/integration/AuditLogRegistrationTest.java index d5cda19..1a08757 100644 --- a/src/test/java/integration/AuditLogRegistrationTest.java +++ b/src/test/java/integration/AuditLogRegistrationTest.java @@ -159,10 +159,6 @@ void getGuild_notRegistered_notFound() { @Test void getGuild_invalidParameters() { - given().contentType("application/json") - .when().get(BASE_PATH + "/notALong") - .then().statusCode(404); - given().contentType("application/json") .when().get(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) .then().statusCode(400); @@ -283,9 +279,6 @@ void deleteGuild_doesNotExist_notFound() { @Test void deleteGuild_invalidParameters() { - given().contentType("application/json") - .when().delete(BASE_PATH + "/notALong") - .then().statusCode(404); given().contentType("application/json") .when().delete(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) diff --git a/src/test/java/integration/MessageLogContentTest.java b/src/test/java/integration/MessageLogContentTest.java index def585d..d41baa0 100644 --- a/src/test/java/integration/MessageLogContentTest.java +++ b/src/test/java/integration/MessageLogContentTest.java @@ -163,10 +163,6 @@ void getMessage_notSaved_notFound() { @Test void getMessage_invalidParameters() { - given().contentType("application/json") - .when().get(BASE_PATH + "/notALong") - .then().statusCode(404); - given().contentType("application/json") .when().get(BASE_PATH + "/" + NEGATIVE_TEST_MESSAGE_ID) .then().statusCode(400); @@ -289,10 +285,6 @@ void deleteMessage_doesNotExist_notFound() { @Test void deleteMessage_invalidParameters() { - given().contentType("application/json") - .when().delete(BASE_PATH + "/notALong") - .then().statusCode(404); - given().contentType("application/json") .when().delete(BASE_PATH + "/" + NEGATIVE_TEST_MESSAGE_ID) .then().statusCode(400); diff --git a/src/test/java/integration/MessageLogRegistrationTest.java b/src/test/java/integration/MessageLogRegistrationTest.java index dfe7951..3f251f5 100644 --- a/src/test/java/integration/MessageLogRegistrationTest.java +++ b/src/test/java/integration/MessageLogRegistrationTest.java @@ -159,10 +159,6 @@ void getGuild_notRegistered_notFound() { @Test void getGuild_invalidParameters() { - given().contentType("application/json") - .when().get(BASE_PATH + "/notALong") - .then().statusCode(404); - given().contentType("application/json") .when().get(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) .then().statusCode(400); @@ -283,9 +279,6 @@ void deleteGuild_doesNotExist_notFound() { @Test void deleteGuild_invalidParameters() { - given().contentType("application/json") - .when().delete(BASE_PATH + "/notALong") - .then().statusCode(404); given().contentType("application/json") .when().delete(BASE_PATH + "/" + NEGATIVE_TEST_GUILD_ID) From 85e4f8a11cd3f7f967e2a5d0c415c428fa85c71f Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:58:11 +0530 Subject: [PATCH 13/27] config: set test logging level to `INFO` - update `quarkus.log.level` from `DEBUG` to `INFO` in `src/test/resources/application.properties` - reduce verbosity in test environment logs Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- src/test/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index f5bedf9..2c76e23 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -15,7 +15,7 @@ quarkus.flyway.default-schema=papertrailbot quarkus.redis.devservices.enabled=true quarkus.redis.devservices.image-name=redis:8.6.1-alpine # Logging Level -quarkus.log.level=DEBUG +quarkus.log.level=INFO # Custom message.locks.enabled=false From 2d6309339762b53e9f36537c4573e120f8cb0eb2 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:02:37 +0530 Subject: [PATCH 14/27] build: skip tests during build - Quarkus manages deploying db and redis containers during tests, which for some reason doesn't work while the tests are from within docker. It seems to require datasource, which Quarkus handles internally. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- Dockerfile.native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.native b/Dockerfile.native index ef68ae2..4c59f13 100644 --- a/Dockerfile.native +++ b/Dockerfile.native @@ -16,7 +16,7 @@ COPY src ./src # Build native binary # quarkus defaults to prod -RUN ./mvnw package -Dnative "-Dquarkus.native.additional-build-args=-J-Djava.net.preferIPv4Stack=true" +RUN ./mvnw clean package -DskipTests -Dnative "-Dquarkus.native.additional-build-args=-J-Djava.net.preferIPv4Stack=true" # ---------- Stage 2: Minimal Runtime ---------- From 0154e2f7bb157523f64f096d5e86830f5740b4e0 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:41:22 +0530 Subject: [PATCH 15/27] chore: update quarkus platform version - update `quarkus.platform.version` from `3.31.3` to `3.31.4` Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c0b25cc..75237dd 100644 --- a/pom.xml +++ b/pom.xml @@ -8,14 +8,13 @@ quarkus - 25 UTF-8 UTF-8 quarkus-bom io.quarkus.platform - 3.31.3 + 3.31.4 true 3.14.1 From 0fee1553ca2e6b13a3d3eb64aaf3ff8e977edffe Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:16:02 +0530 Subject: [PATCH 16/27] chore: remove h2 dependency Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pom.xml b/pom.xml index 75237dd..f8ca70b 100644 --- a/pom.xml +++ b/pom.xml @@ -68,10 +68,6 @@ io.quarkus quarkus-hibernate-orm-panache - - io.quarkus - quarkus-jdbc-h2 - io.quarkus quarkus-jdbc-postgresql From dc29b4f956fecb94ee4f580947c3a83a67fcaeb2 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:18:19 +0530 Subject: [PATCH 17/27] chore(dev-config): activate quarkus devservices - rely on quarkus devservices for deploying test database and redis, instead of using H2 for db - Set `quarkus.hibernate-orm.schema-management.strategy` to `validate`. - Change `quarkus.log.level` to `INFO`. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- src/main/resources/application-dev.properties | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 73d11b3..fd9bcf6 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,24 +1,18 @@ # Database -quarkus.datasource.db-kind=h2 -quarkus.datasource.username=sa -quarkus.datasource.password= +quarkus.datasource.db-kind=postgresql +quarkus.datasource.devservices.image-name=postgres:18.2-alpine -quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 -quarkus.datasource.jdbc.max-size=16 quarkus.hibernate-orm.database.default-schema=papertrailbot -quarkus.hibernate-orm.schema-management.strategy=drop-and-create +quarkus.hibernate-orm.schema-management.strategy=validate + # Flyway quarkus.flyway.migrate-at-start=true quarkus.flyway.create-schemas=true quarkus.flyway.schemas=papertrailbot quarkus.flyway.default-schema=papertrailbot - -# Global Redis Settings -# (don't configure in dev mode so that dev tools can automatically spin up a container having redis if ur docker is running) -# quarkus.redis.hosts=redis://localhost:6379 -# quarkus.redis.password= - +# Redis quarkus.cache.type=redis +quarkus.redis.devservices.image-name=redis:8.6.1-alpine # Redis Cache Settings quarkus.cache.redis."auditLog".ttl=30D @@ -31,7 +25,7 @@ quarkus.redisson.threads=16 quarkus.redisson.netty-threads=32 # Logging Level -quarkus.log.level=DEBUG +quarkus.log.level=INFO # Analytics quarkus.analytics.disabled=true From a33ebcb8e0ceec50cd363767aa5e1f527d6ad7a7 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:35:03 +0530 Subject: [PATCH 18/27] refactor(service): standardize delete operation error handling - Remove redundant `findByIdOptional` checks before deletion - Throw specific exceptions when delete operations fail, ensuring consistent error propagation Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../api/service/AuditLogRegistrationService.java | 7 +------ .../papertrail/api/service/MessageLogContentService.java | 6 +----- .../api/service/MessageLogRegistrationService.java | 1 - 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java b/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java index 6578b94..44669dd 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java @@ -18,7 +18,6 @@ import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; - @ApplicationScoped @RequiredArgsConstructor @Slf4j @@ -69,13 +68,9 @@ public class AuditLogRegistrationService { @CacheInvalidate(cacheName = "auditLog") public void deleteRegisteredGuild(@NonNull @CacheKey Long guildId) { - repository - .findByIdOptional(guildId) - .orElseThrow(() -> new GuildNotFoundException("Guild is not registered for audit logging")); - if (repository.deleteById(guildId)) log.debug("{}Deleted audit log guild with ID={}{}", AnsiColor.GREEN, guildId, AnsiColor.RESET); else - log.warn("{}Failed to delete audit log guild with ID={}{}", AnsiColor.YELLOW, guildId, AnsiColor.RESET); + throw new GuildNotFoundException("Guild is not registered for audit logging"); } } diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java index 57a74c8..51ae6f5 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java @@ -77,14 +77,10 @@ public class MessageLogContentService { @CacheInvalidate(cacheName = "messageContent") public void deleteMessage(@NonNull @CacheKey Long messageId) { - repository - .findByIdOptional(messageId) - .orElseThrow(() -> new MessageNotFoundException("Message to be deleted was never saved")); - if (repository.deleteById(messageId)) log.debug("{} Deleted message having ID={}{}", AnsiColor.GREEN, messageId, AnsiColor.RESET); else - log.warn("{}Failed to delete message having ID={}{}", AnsiColor.YELLOW, messageId, AnsiColor.RESET); + throw new MessageNotFoundException("Message to be deleted was never saved"); } @Scheduled(every = "24h") diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java index 4e96068..d0197a3 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java @@ -18,7 +18,6 @@ import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; - @ApplicationScoped @RequiredArgsConstructor @Slf4j From 21a5b48504e48e3294438a82154bbf6a8651fd2d Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:36:25 +0530 Subject: [PATCH 19/27] refactor(integration-tests): rename integration test files and classes - rename integration test files from `*Test.java` to `*ServiceIntegrationTest.java` Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- ...est.java => AuditLogRegistrationServiceIntegrationTest.java} | 2 +- ...ntTest.java => MessageLogContentServiceIntegrationTest.java} | 2 +- ...t.java => MessageLogRegistrationServiceIntegrationTest.java} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/test/java/integration/{AuditLogRegistrationTest.java => AuditLogRegistrationServiceIntegrationTest.java} (99%) rename src/test/java/integration/{MessageLogContentTest.java => MessageLogContentServiceIntegrationTest.java} (99%) rename src/test/java/integration/{MessageLogRegistrationTest.java => MessageLogRegistrationServiceIntegrationTest.java} (99%) diff --git a/src/test/java/integration/AuditLogRegistrationTest.java b/src/test/java/integration/AuditLogRegistrationServiceIntegrationTest.java similarity index 99% rename from src/test/java/integration/AuditLogRegistrationTest.java rename to src/test/java/integration/AuditLogRegistrationServiceIntegrationTest.java index 1a08757..f2cd158 100644 --- a/src/test/java/integration/AuditLogRegistrationTest.java +++ b/src/test/java/integration/AuditLogRegistrationServiceIntegrationTest.java @@ -20,7 +20,7 @@ import static org.hamcrest.Matchers.is; @QuarkusTest -class AuditLogRegistrationTest { +class AuditLogRegistrationServiceIntegrationTest { private static final String BASE_PATH = "/api/v1/log/audit"; diff --git a/src/test/java/integration/MessageLogContentTest.java b/src/test/java/integration/MessageLogContentServiceIntegrationTest.java similarity index 99% rename from src/test/java/integration/MessageLogContentTest.java rename to src/test/java/integration/MessageLogContentServiceIntegrationTest.java index d41baa0..69f94ab 100644 --- a/src/test/java/integration/MessageLogContentTest.java +++ b/src/test/java/integration/MessageLogContentServiceIntegrationTest.java @@ -20,7 +20,7 @@ import static org.hamcrest.Matchers.is; @QuarkusTest -class MessageLogContentTest { +class MessageLogContentServiceIntegrationTest { private static final String BASE_PATH = "/api/v1/content/message"; diff --git a/src/test/java/integration/MessageLogRegistrationTest.java b/src/test/java/integration/MessageLogRegistrationServiceIntegrationTest.java similarity index 99% rename from src/test/java/integration/MessageLogRegistrationTest.java rename to src/test/java/integration/MessageLogRegistrationServiceIntegrationTest.java index 3f251f5..73aa315 100644 --- a/src/test/java/integration/MessageLogRegistrationTest.java +++ b/src/test/java/integration/MessageLogRegistrationServiceIntegrationTest.java @@ -20,7 +20,7 @@ import static org.hamcrest.Matchers.is; @QuarkusTest -class MessageLogRegistrationTest { +class MessageLogRegistrationServiceIntegrationTest { private static final String BASE_PATH = "/api/v1/log/message"; From acca633bb6bbae35eeee460dabd96d0c1bfaf764 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:53:12 +0530 Subject: [PATCH 20/27] build: add quarkus junit mockito test dependency Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index f8ca70b..1b205d4 100644 --- a/pom.xml +++ b/pom.xml @@ -137,6 +137,11 @@ quarkus-junit test + + io.quarkus + quarkus-junit-mockito + test + io.rest-assured rest-assured From 0901051eb87d60ee7ff6493b0dec0485ba3edfd3 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:53:53 +0530 Subject: [PATCH 21/27] test(audit-log-registration-service): add unit tests Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../AuditLogRegistrationServiceUnitTest.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/test/java/unit/AuditLogRegistrationServiceUnitTest.java diff --git a/src/test/java/unit/AuditLogRegistrationServiceUnitTest.java b/src/test/java/unit/AuditLogRegistrationServiceUnitTest.java new file mode 100644 index 0000000..aa48a0d --- /dev/null +++ b/src/test/java/unit/AuditLogRegistrationServiceUnitTest.java @@ -0,0 +1,142 @@ +package unit; + +import io.github.eggy03.papertrail.api.dto.AuditLogRegistrationDTO; +import io.github.eggy03.papertrail.api.entity.AuditLogRegistration; +import io.github.eggy03.papertrail.api.exceptions.GuildNotFoundException; +import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationException; +import io.github.eggy03.papertrail.api.mapper.AuditLogRegistrationMapper; +import io.github.eggy03.papertrail.api.repository.AuditLogRegistrationRepository; +import io.github.eggy03.papertrail.api.service.AuditLogRegistrationService; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AuditLogRegistrationServiceUnitTest { + + static final Long TEST_GUILD_ID = 1302148573926148096L; + static final Long TEST_CHANNEL_ID = 1302148573926148097L; + // prep a valid Entity + final AuditLogRegistration validEntity = new AuditLogRegistration(TEST_GUILD_ID, TEST_CHANNEL_ID); + // prep a valid DTO + final AuditLogRegistrationDTO validDTO = new AuditLogRegistrationDTO(TEST_GUILD_ID, TEST_CHANNEL_ID); + @Mock + AuditLogRegistrationRepository repository; + @Mock + AuditLogRegistrationMapper mapper; + @InjectMocks + AuditLogRegistrationService service; + + @Test + void registerGuild_success() { + + when(mapper.toEntity(validDTO)).thenReturn(validEntity); + + service.registerGuild(validDTO); + + verify(repository).persistAndFlush(validEntity); + verifyNoMoreInteractions(repository, mapper); + } + + @Test + void registerGuild_alreadyExists_conflicts() { + + when(mapper.toEntity(validDTO)).thenReturn(validEntity); + doThrow(ConstraintViolationException.class).when(repository).persistAndFlush(validEntity); + + assertThrows(GuildRegistrationException.class, () -> service.registerGuild(validDTO)); + + verify(mapper).toEntity(validDTO); + verify(repository).persistAndFlush(validEntity); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void getGuild_success() { + + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.of(validEntity)); + when(mapper.toDTO(validEntity)).thenReturn(validDTO); + + AuditLogRegistrationDTO result = service.viewRegisteredGuild(TEST_GUILD_ID); + assertThat(result).isEqualTo(validDTO); + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verify(mapper).toDTO(validEntity); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void getGuild_notRegistered_notFound() { + + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.empty()); + + assertThrows(GuildNotFoundException.class, () -> service.viewRegisteredGuild(TEST_GUILD_ID)); + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verify(mapper, never()).toDTO(any()); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void updateGuild_success() { + + AuditLogRegistration oldEntity = new AuditLogRegistration(TEST_GUILD_ID, 123L); + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.of(oldEntity)); + + service.updateRegisteredGuild(TEST_GUILD_ID, validDTO); + assertThat(oldEntity.getChannelId()).isEqualTo(validDTO.getChannelId()); // confirm that old entity was mutated with new dto data + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + + } + + @Test + void updateGuild_doesNotExist() { + + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.empty()); + + assertThrows(GuildNotFoundException.class, () -> service.updateRegisteredGuild(TEST_GUILD_ID, validDTO)); + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + } + + + @Test + void deleteGuild_success() { + + when(repository.deleteById(TEST_GUILD_ID)).thenReturn(true); + + assertDoesNotThrow(() -> service.deleteRegisteredGuild(TEST_GUILD_ID)); + + verify(repository).deleteById(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + } + + @Test + void deleteGuild_doesNotExist_notFound() { + + when(repository.deleteById(TEST_GUILD_ID)).thenReturn(false); + + assertThrows(GuildNotFoundException.class, () -> service.deleteRegisteredGuild(TEST_GUILD_ID)); + + verify(repository).deleteById(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + } +} From 9dcbf09e656fbe2049cde5b2e855fdcad1f0bb4e Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:58:23 +0530 Subject: [PATCH 22/27] fix: apply missed out optimizations in a33ebcb Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../api/service/MessageLogRegistrationService.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java index d0197a3..3f1cf63 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java @@ -68,12 +68,9 @@ public class MessageLogRegistrationService { @CacheInvalidate(cacheName = "messageLog") public void deleteRegisteredGuild(@NonNull @CacheKey Long guildId) { - repository.findByIdOptional(guildId) - .orElseThrow(() -> new GuildNotFoundException("Guild is not registered for message logging")); - if (repository.deleteById(guildId)) log.debug("{} Deleted message log guild with ID={}{}", AnsiColor.GREEN, guildId, AnsiColor.RESET); else - log.warn("{}Failed to delete message log guild with ID={}{}", AnsiColor.YELLOW, guildId, AnsiColor.RESET); + throw new GuildNotFoundException("Guild is not registered for message logging"); } } From d9f4a2d0a54a985448302de467191d7ccbb9dd4a Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:59:31 +0530 Subject: [PATCH 23/27] test(message-log-registration-service): add unit tests Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- ...MessageLogRegistrationServiceUnitTest.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/test/java/unit/MessageLogRegistrationServiceUnitTest.java diff --git a/src/test/java/unit/MessageLogRegistrationServiceUnitTest.java b/src/test/java/unit/MessageLogRegistrationServiceUnitTest.java new file mode 100644 index 0000000..1c834eb --- /dev/null +++ b/src/test/java/unit/MessageLogRegistrationServiceUnitTest.java @@ -0,0 +1,142 @@ +package unit; + +import io.github.eggy03.papertrail.api.dto.MessageLogRegistrationDTO; +import io.github.eggy03.papertrail.api.entity.MessageLogRegistration; +import io.github.eggy03.papertrail.api.exceptions.GuildNotFoundException; +import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationException; +import io.github.eggy03.papertrail.api.mapper.MessageLogRegistrationMapper; +import io.github.eggy03.papertrail.api.repository.MessageLogRegistrationRepository; +import io.github.eggy03.papertrail.api.service.MessageLogRegistrationService; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MessageLogRegistrationServiceUnitTest { + + static final Long TEST_GUILD_ID = 1302148573926148096L; + static final Long TEST_CHANNEL_ID = 1302148573926148097L; + // prep a valid Entity + final MessageLogRegistration validEntity = new MessageLogRegistration(TEST_GUILD_ID, TEST_CHANNEL_ID); + // prep a valid DTO + final MessageLogRegistrationDTO validDTO = new MessageLogRegistrationDTO(TEST_GUILD_ID, TEST_CHANNEL_ID); + @Mock + MessageLogRegistrationRepository repository; + @Mock + MessageLogRegistrationMapper mapper; + @InjectMocks + MessageLogRegistrationService service; + + @Test + void registerGuild_success() { + + when(mapper.toEntity(validDTO)).thenReturn(validEntity); + + service.registerGuild(validDTO); + + verify(repository).persistAndFlush(validEntity); + verifyNoMoreInteractions(repository, mapper); + } + + @Test + void registerGuild_alreadyExists_conflicts() { + + when(mapper.toEntity(validDTO)).thenReturn(validEntity); + doThrow(ConstraintViolationException.class).when(repository).persistAndFlush(validEntity); + + assertThrows(GuildRegistrationException.class, () -> service.registerGuild(validDTO)); + + verify(mapper).toEntity(validDTO); + verify(repository).persistAndFlush(validEntity); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void getGuild_success() { + + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.of(validEntity)); + when(mapper.toDTO(validEntity)).thenReturn(validDTO); + + MessageLogRegistrationDTO result = service.viewRegisteredGuild(TEST_GUILD_ID); + assertThat(result).isEqualTo(validDTO); + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verify(mapper).toDTO(validEntity); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void getGuild_notRegistered_notFound() { + + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.empty()); + + assertThrows(GuildNotFoundException.class, () -> service.viewRegisteredGuild(TEST_GUILD_ID)); + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verify(mapper, never()).toDTO(any()); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void updateGuild_success() { + + MessageLogRegistration oldEntity = new MessageLogRegistration(TEST_GUILD_ID, 123L); + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.of(oldEntity)); + + service.updateRegisteredGuild(TEST_GUILD_ID, validDTO); + assertThat(oldEntity.getChannelId()).isEqualTo(validDTO.getChannelId()); // confirm that old entity was mutated with new dto data + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + + } + + @Test + void updateGuild_doesNotExist() { + + when(repository.findByIdOptional(TEST_GUILD_ID)).thenReturn(Optional.empty()); + + assertThrows(GuildNotFoundException.class, () -> service.updateRegisteredGuild(TEST_GUILD_ID, validDTO)); + + verify(repository).findByIdOptional(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + } + + + @Test + void deleteGuild_success() { + + when(repository.deleteById(TEST_GUILD_ID)).thenReturn(true); + + assertDoesNotThrow(() -> service.deleteRegisteredGuild(TEST_GUILD_ID)); + + verify(repository).deleteById(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + } + + @Test + void deleteGuild_doesNotExist_notFound() { + + when(repository.deleteById(TEST_GUILD_ID)).thenReturn(false); + + assertThrows(GuildNotFoundException.class, () -> service.deleteRegisteredGuild(TEST_GUILD_ID)); + + verify(repository).deleteById(TEST_GUILD_ID); + verifyNoMoreInteractions(repository); + } +} From 57a3922a3fb93899ce39a0522a82a14f06397376 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:20:53 +0530 Subject: [PATCH 24/27] refactor(exceptions): rename exception and mapper classes for clarity - Rename `MessageContentException` and `GuildRegistrationException` to include "Failure" suffix. - Update corresponding exception mappers and service/test usages. Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- ...xception.java => GuildRegistrationFailureException.java} | 2 +- ...ationException.java => MessageSaveFailureException.java} | 2 +- ...er.java => GuildRegistrationFailureExceptionMapper.java} | 6 +++--- ...onMapper.java => MessageSaveFailureExceptionMapper.java} | 6 +++--- .../papertrail/api/service/AuditLogRegistrationService.java | 4 ++-- .../papertrail/api/service/MessageLogContentService.java | 4 ++-- .../api/service/MessageLogRegistrationService.java | 4 ++-- src/test/java/unit/AuditLogRegistrationServiceUnitTest.java | 4 ++-- .../java/unit/MessageLogRegistrationServiceUnitTest.java | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) rename src/main/java/io/github/eggy03/papertrail/api/exceptions/{MessageContentException.java => GuildRegistrationFailureException.java} (62%) rename src/main/java/io/github/eggy03/papertrail/api/exceptions/{GuildRegistrationException.java => MessageSaveFailureException.java} (64%) rename src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/{GuildRegistrationExceptionMapper.java => GuildRegistrationFailureExceptionMapper.java} (83%) rename src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/{MessageContentExceptionMapper.java => MessageSaveFailureExceptionMapper.java} (80%) diff --git a/src/main/java/io/github/eggy03/papertrail/api/exceptions/MessageContentException.java b/src/main/java/io/github/eggy03/papertrail/api/exceptions/GuildRegistrationFailureException.java similarity index 62% rename from src/main/java/io/github/eggy03/papertrail/api/exceptions/MessageContentException.java rename to src/main/java/io/github/eggy03/papertrail/api/exceptions/GuildRegistrationFailureException.java index 104f763..f3af97c 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/exceptions/MessageContentException.java +++ b/src/main/java/io/github/eggy03/papertrail/api/exceptions/GuildRegistrationFailureException.java @@ -3,5 +3,5 @@ import lombok.experimental.StandardException; @StandardException -public class MessageContentException extends RuntimeException { +public class GuildRegistrationFailureException extends RuntimeException { } diff --git a/src/main/java/io/github/eggy03/papertrail/api/exceptions/GuildRegistrationException.java b/src/main/java/io/github/eggy03/papertrail/api/exceptions/MessageSaveFailureException.java similarity index 64% rename from src/main/java/io/github/eggy03/papertrail/api/exceptions/GuildRegistrationException.java rename to src/main/java/io/github/eggy03/papertrail/api/exceptions/MessageSaveFailureException.java index 778851b..d78c27b 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/exceptions/GuildRegistrationException.java +++ b/src/main/java/io/github/eggy03/papertrail/api/exceptions/MessageSaveFailureException.java @@ -3,5 +3,5 @@ import lombok.experimental.StandardException; @StandardException -public class GuildRegistrationException extends RuntimeException { +public class MessageSaveFailureException extends RuntimeException { } diff --git a/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/GuildRegistrationExceptionMapper.java b/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/GuildRegistrationFailureExceptionMapper.java similarity index 83% rename from src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/GuildRegistrationExceptionMapper.java rename to src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/GuildRegistrationFailureExceptionMapper.java index 762b662..c446508 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/GuildRegistrationExceptionMapper.java +++ b/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/GuildRegistrationFailureExceptionMapper.java @@ -1,6 +1,6 @@ package io.github.eggy03.papertrail.api.exceptions.mapper; -import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationException; +import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationFailureException; import io.github.eggy03.papertrail.api.exceptions.entity.ErrorResponse; import io.github.eggy03.papertrail.api.util.AnsiColor; import jakarta.ws.rs.core.Context; @@ -14,13 +14,13 @@ @Provider @Slf4j -public class GuildRegistrationExceptionMapper implements ExceptionMapper { +public class GuildRegistrationFailureExceptionMapper implements ExceptionMapper { @Context UriInfo uriInfo; @Override - public Response toResponse(GuildRegistrationException e) { + public Response toResponse(GuildRegistrationFailureException e) { log.debug(AnsiColor.MAGENTA + "{}" + AnsiColor.RESET, e.getMessage(), e); diff --git a/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/MessageContentExceptionMapper.java b/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/MessageSaveFailureExceptionMapper.java similarity index 80% rename from src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/MessageContentExceptionMapper.java rename to src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/MessageSaveFailureExceptionMapper.java index 865d2b8..cea19e1 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/MessageContentExceptionMapper.java +++ b/src/main/java/io/github/eggy03/papertrail/api/exceptions/mapper/MessageSaveFailureExceptionMapper.java @@ -1,6 +1,6 @@ package io.github.eggy03.papertrail.api.exceptions.mapper; -import io.github.eggy03.papertrail.api.exceptions.MessageContentException; +import io.github.eggy03.papertrail.api.exceptions.MessageSaveFailureException; import io.github.eggy03.papertrail.api.exceptions.entity.ErrorResponse; import io.github.eggy03.papertrail.api.util.AnsiColor; import jakarta.ws.rs.core.Context; @@ -14,13 +14,13 @@ @Provider @Slf4j -public class MessageContentExceptionMapper implements ExceptionMapper { +public class MessageSaveFailureExceptionMapper implements ExceptionMapper { @Context UriInfo uriInfo; @Override - public Response toResponse(MessageContentException e) { + public Response toResponse(MessageSaveFailureException e) { log.debug(AnsiColor.MAGENTA + "{}" + AnsiColor.RESET, e.getMessage(), e); diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java b/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java index 44669dd..61e8b1f 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/AuditLogRegistrationService.java @@ -3,7 +3,7 @@ import io.github.eggy03.papertrail.api.dto.AuditLogRegistrationDTO; import io.github.eggy03.papertrail.api.entity.AuditLogRegistration; import io.github.eggy03.papertrail.api.exceptions.GuildNotFoundException; -import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationException; +import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationFailureException; import io.github.eggy03.papertrail.api.mapper.AuditLogRegistrationMapper; import io.github.eggy03.papertrail.api.repository.AuditLogRegistrationRepository; import io.github.eggy03.papertrail.api.util.AnsiColor; @@ -34,7 +34,7 @@ public class AuditLogRegistrationService { log.debug("{}Saved audit log guild with ID={}{}", AnsiColor.GREEN, dto.getGuildId(), AnsiColor.RESET); return dto; } catch (ConstraintViolationException e) { // from hibernate - throw new GuildRegistrationException(e); + throw new GuildRegistrationFailureException(e); } } diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java index 51ae6f5..f907821 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogContentService.java @@ -2,8 +2,8 @@ import io.github.eggy03.papertrail.api.dto.MessageLogContentDTO; import io.github.eggy03.papertrail.api.entity.MessageLogContent; -import io.github.eggy03.papertrail.api.exceptions.MessageContentException; import io.github.eggy03.papertrail.api.exceptions.MessageNotFoundException; +import io.github.eggy03.papertrail.api.exceptions.MessageSaveFailureException; import io.github.eggy03.papertrail.api.mapper.MessageLogContentMapper; import io.github.eggy03.papertrail.api.repository.MessageLogContentRepository; import io.github.eggy03.papertrail.api.util.AnsiColor; @@ -38,7 +38,7 @@ public class MessageLogContentService { log.debug("{}Saved message with ID={}{}", AnsiColor.GREEN, dto.getMessageId(), AnsiColor.RESET); return dto; } catch (ConstraintViolationException e) {// from hibernate - throw new MessageContentException(e); + throw new MessageSaveFailureException(e); } // API Note: While ConstraintViolationException covers for a lot of constraints other than PK constraint // We have already covered them during dto validation phase in the controller diff --git a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java index 3f1cf63..7d68b7c 100644 --- a/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java +++ b/src/main/java/io/github/eggy03/papertrail/api/service/MessageLogRegistrationService.java @@ -3,7 +3,7 @@ import io.github.eggy03.papertrail.api.dto.MessageLogRegistrationDTO; import io.github.eggy03.papertrail.api.entity.MessageLogRegistration; import io.github.eggy03.papertrail.api.exceptions.GuildNotFoundException; -import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationException; +import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationFailureException; import io.github.eggy03.papertrail.api.mapper.MessageLogRegistrationMapper; import io.github.eggy03.papertrail.api.repository.MessageLogRegistrationRepository; import io.github.eggy03.papertrail.api.util.AnsiColor; @@ -34,7 +34,7 @@ public class MessageLogRegistrationService { log.debug("{}Saved message log guild with ID={}{}", AnsiColor.GREEN, dto.getGuildId(), AnsiColor.RESET); return dto; } catch (ConstraintViolationException e) { // from hibernate - throw new GuildRegistrationException(e); + throw new GuildRegistrationFailureException(e); } } diff --git a/src/test/java/unit/AuditLogRegistrationServiceUnitTest.java b/src/test/java/unit/AuditLogRegistrationServiceUnitTest.java index aa48a0d..b62d666 100644 --- a/src/test/java/unit/AuditLogRegistrationServiceUnitTest.java +++ b/src/test/java/unit/AuditLogRegistrationServiceUnitTest.java @@ -3,7 +3,7 @@ import io.github.eggy03.papertrail.api.dto.AuditLogRegistrationDTO; import io.github.eggy03.papertrail.api.entity.AuditLogRegistration; import io.github.eggy03.papertrail.api.exceptions.GuildNotFoundException; -import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationException; +import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationFailureException; import io.github.eggy03.papertrail.api.mapper.AuditLogRegistrationMapper; import io.github.eggy03.papertrail.api.repository.AuditLogRegistrationRepository; import io.github.eggy03.papertrail.api.service.AuditLogRegistrationService; @@ -59,7 +59,7 @@ void registerGuild_alreadyExists_conflicts() { when(mapper.toEntity(validDTO)).thenReturn(validEntity); doThrow(ConstraintViolationException.class).when(repository).persistAndFlush(validEntity); - assertThrows(GuildRegistrationException.class, () -> service.registerGuild(validDTO)); + assertThrows(GuildRegistrationFailureException.class, () -> service.registerGuild(validDTO)); verify(mapper).toEntity(validDTO); verify(repository).persistAndFlush(validEntity); diff --git a/src/test/java/unit/MessageLogRegistrationServiceUnitTest.java b/src/test/java/unit/MessageLogRegistrationServiceUnitTest.java index 1c834eb..fde0968 100644 --- a/src/test/java/unit/MessageLogRegistrationServiceUnitTest.java +++ b/src/test/java/unit/MessageLogRegistrationServiceUnitTest.java @@ -3,7 +3,7 @@ import io.github.eggy03.papertrail.api.dto.MessageLogRegistrationDTO; import io.github.eggy03.papertrail.api.entity.MessageLogRegistration; import io.github.eggy03.papertrail.api.exceptions.GuildNotFoundException; -import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationException; +import io.github.eggy03.papertrail.api.exceptions.GuildRegistrationFailureException; import io.github.eggy03.papertrail.api.mapper.MessageLogRegistrationMapper; import io.github.eggy03.papertrail.api.repository.MessageLogRegistrationRepository; import io.github.eggy03.papertrail.api.service.MessageLogRegistrationService; @@ -59,7 +59,7 @@ void registerGuild_alreadyExists_conflicts() { when(mapper.toEntity(validDTO)).thenReturn(validEntity); doThrow(ConstraintViolationException.class).when(repository).persistAndFlush(validEntity); - assertThrows(GuildRegistrationException.class, () -> service.registerGuild(validDTO)); + assertThrows(GuildRegistrationFailureException.class, () -> service.registerGuild(validDTO)); verify(mapper).toEntity(validDTO); verify(repository).persistAndFlush(validEntity); From 08539a3c65fa314320273285ff4002e302e63863 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:25:47 +0530 Subject: [PATCH 25/27] test(message-log-content-service): add unit tests Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- .../MessageLogContentServiceUnitTest.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/test/java/unit/MessageLogContentServiceUnitTest.java diff --git a/src/test/java/unit/MessageLogContentServiceUnitTest.java b/src/test/java/unit/MessageLogContentServiceUnitTest.java new file mode 100644 index 0000000..f8983b4 --- /dev/null +++ b/src/test/java/unit/MessageLogContentServiceUnitTest.java @@ -0,0 +1,144 @@ +package unit; + +import io.github.eggy03.papertrail.api.dto.MessageLogContentDTO; +import io.github.eggy03.papertrail.api.entity.MessageLogContent; +import io.github.eggy03.papertrail.api.exceptions.MessageNotFoundException; +import io.github.eggy03.papertrail.api.exceptions.MessageSaveFailureException; +import io.github.eggy03.papertrail.api.mapper.MessageLogContentMapper; +import io.github.eggy03.papertrail.api.repository.MessageLogContentRepository; +import io.github.eggy03.papertrail.api.service.MessageLogContentService; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MessageLogContentServiceUnitTest { + + static final Long TEST_MESSAGE_ID = 1302148573926148096L; + static final String TEST_MESSAGE_CONTENT = "message"; + static final Long TEST_AUTHOR_ID = 1302148573926148097L; + // prep a valid Entity + final MessageLogContent validEntity = new MessageLogContent(TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, TEST_AUTHOR_ID, null); + // prep a valid DTO + final MessageLogContentDTO validDTO = new MessageLogContentDTO(TEST_MESSAGE_ID, TEST_MESSAGE_CONTENT, TEST_AUTHOR_ID); + @Mock + MessageLogContentRepository repository; + @Mock + MessageLogContentMapper mapper; + @InjectMocks + MessageLogContentService service; + + @Test + void saveMessage_success() { + + when(mapper.toEntity(validDTO)).thenReturn(validEntity); + + service.saveMessage(validDTO); + + verify(repository).persistAndFlush(validEntity); + verifyNoMoreInteractions(repository, mapper); + } + + @Test + void saveMessage_alreadyExists_conflicts() { + + when(mapper.toEntity(validDTO)).thenReturn(validEntity); + doThrow(ConstraintViolationException.class).when(repository).persistAndFlush(validEntity); + + assertThrows(MessageSaveFailureException.class, () -> service.saveMessage(validDTO)); + + verify(mapper).toEntity(validDTO); + verify(repository).persistAndFlush(validEntity); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void getMessage_success() { + + when(repository.findByIdOptional(TEST_MESSAGE_ID)).thenReturn(Optional.of(validEntity)); + when(mapper.toDTO(validEntity)).thenReturn(validDTO); + + MessageLogContentDTO result = service.getMessage(TEST_MESSAGE_ID); + assertThat(result).isEqualTo(validDTO); + + verify(repository).findByIdOptional(TEST_MESSAGE_ID); + verify(mapper).toDTO(validEntity); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void getMessage_notSaved_notFound() { + + when(repository.findByIdOptional(TEST_MESSAGE_ID)).thenReturn(Optional.empty()); + + assertThrows(MessageNotFoundException.class, () -> service.getMessage(TEST_MESSAGE_ID)); + + verify(repository).findByIdOptional(TEST_MESSAGE_ID); + verify(mapper, never()).toDTO(any()); + verifyNoMoreInteractions(mapper, repository); + } + + @Test + void updateMessage_success() { + + MessageLogContent oldEntity = new MessageLogContent(TEST_MESSAGE_ID, "oldMessage", 123L, null); + when(repository.findByIdOptional(TEST_MESSAGE_ID)).thenReturn(Optional.of(oldEntity)); + + service.updateMessage(TEST_MESSAGE_ID, validDTO); + assertThat(oldEntity.getMessageContent()).isEqualTo(validDTO.getMessageContent()); + assertThat(oldEntity.getAuthorId()).isEqualTo(validDTO.getAuthorId());// confirm that old entity was mutated with new dto data + + verify(repository).findByIdOptional(TEST_MESSAGE_ID); + verifyNoMoreInteractions(repository); + + } + + @Test + void updateMessage_doesNotExist() { + + when(repository.findByIdOptional(TEST_MESSAGE_ID)).thenReturn(Optional.empty()); + + assertThrows(MessageNotFoundException.class, () -> service.updateMessage(TEST_MESSAGE_ID, validDTO)); + + verify(repository).findByIdOptional(TEST_MESSAGE_ID); + verifyNoMoreInteractions(repository); + } + + + @Test + void deleteMessage_success() { + + when(repository.deleteById(TEST_MESSAGE_ID)).thenReturn(true); + + assertDoesNotThrow(() -> service.deleteMessage(TEST_MESSAGE_ID)); + + verify(repository).deleteById(TEST_MESSAGE_ID); + verifyNoMoreInteractions(repository); + } + + @Test + void deleteMessage_doesNotExist_notFound() { + + when(repository.deleteById(TEST_MESSAGE_ID)).thenReturn(false); + + assertThrows(MessageNotFoundException.class, () -> service.deleteMessage(TEST_MESSAGE_ID)); + + verify(repository).deleteById(TEST_MESSAGE_ID); + verifyNoMoreInteractions(repository); + } +} From bd7c3babbbdc4c73ee4ab11c5675d8a68efc2933 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:41:19 +0530 Subject: [PATCH 26/27] docs: relocate migration guide - relocate the "Migration Guide" section to its own top-level heading - change migration guide admonition from `!IMPORTANT` to `!NOTE` Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- README.md | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 00c836b..d6bc772 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ API service for the PaperTrail Bot, built with Quarkus 3 and optimized for nativ ### Services Required -| Service | Version | -|-----------------------|----------------| -| `Relational Database` | Postgres | -| `Distributed Cache` | Redis / Valkey | +| Service Type | Supported Variants | +|-----------------------|--------------------| +| `Relational Database` | Postgres | +| `Distributed Cache` | Redis / Valkey | It is recommended that you deploy the latest or the officially supported versions of Postgres and Redis or Valkey. At the time of development, Postgres 17 and 18 and Valkey 8 were fully supported. @@ -35,25 +35,6 @@ Below are the links to the official docs stating the support status of each of t - [Redis Supported Versions](https://redis.io/docs/latest/operate/rs/references/supported-platforms/) - [Valkey Supported Versions](https://valkey.io/topics/releases/) -> [!IMPORTANT] -> This section applies only to users migrating from the Spring-based API to this API. -> -> Depending on your existing database setup, you may encounter up to **two** breaking changes: -> -> **Case-1**: Using a database other than PostgreSQL -> -> You need to migrate your existing data to a newly created Postgres DB. -> This API exclusively supports Postgres. Support for other DBs have been dropped to ease maintainability. -> -> **Case-2**: Already using Postgres -> -> There is only **one** breaking change: -> - Previously, tables were created in the default schema. -> - The new API uses flyway to check and create tables in a custom schema named `papertrailbot` on startup. -> -> The table structures and relationships remain unchanged. -> You only need to migrate your existing data from the default schema to the `papertrailbot` schema. - ### Environment Variables Required | Variable | Description | @@ -156,6 +137,28 @@ Some cloud platforms may require these endpoints to periodically determine the h > /q/health - Accumulates all health check procedures in the application. +# Migration Guide + +> [!NOTE] +> This section applies only to users migrating from the Spring-based API. + +Depending on your existing database setup, you may encounter up to **two** breaking changes: + +**Case-1**: Using a database other than PostgreSQL + +You need to migrate your existing data to a newly created Postgres DB. +This API exclusively supports Postgres. Support for other DBs have been dropped to ease maintainability. + +**Case-2**: Already using Postgres + +There is only **one** breaking change: + +- Previously, tables were created in the default schema. +- The new API uses flyway to check and create tables in a custom schema named `papertrailbot` on startup. + +The table structures and relationships remain unchanged. +You only need to migrate your existing data from the default schema to the `papertrailbot` schema. + # License This API is licensed under the [AGPLv3](/LICENSE) license. From 1b19cbc0edc2a2bc802d0f85829854da70d232b4 Mon Sep 17 00:00:00 2001 From: Egg-03 <111327101+eggy03@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:46:50 +0530 Subject: [PATCH 27/27] build(deps): update quarkus platform and project version - update `quarkus.platform.version` from `3.31.4` to `3.32.1` - update project version from `1.0.0-SNAPSHOT` to `1.0.0` Signed-off-by: Egg-03 <111327101+eggy03@users.noreply.github.com> --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1b205d4..c3c60ac 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.github.eggy03 papertrail-api-quarkus - 1.0.0-SNAPSHOT + 1.0.0 quarkus @@ -14,7 +14,7 @@ quarkus-bom io.quarkus.platform - 3.31.4 + 3.32.1 true 3.14.1