From 8502430ffff23c08231847baf771f13028d1e11b Mon Sep 17 00:00:00 2001 From: Juanjo Aguililla Date: Wed, 11 Sep 2024 07:26:31 +0200 Subject: [PATCH 1/6] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..18c9147 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From 9ab0dcb039a317193d9ec2e2bfc44c00b3012a5d Mon Sep 17 00:00:00 2001 From: jaguililla Date: Wed, 11 Sep 2024 07:51:34 +0200 Subject: [PATCH 2/6] Update change request templates --- .github/pull_request_template.md | 1 - .gitlab/merge_request_templates/mr.md | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f177803..a420f6f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,7 +9,6 @@ For the Pull Request to be accepted please check: - [ ] If the PR refers to an issue, it should be referenced with the GitHub format (*#ID*). - [ ] The PR is done to the `develop` branch (new features) or the `master` branch (releases). - [ ] The code pass all PR checks. -- [ ] All public members are documented. - [ ] The code follow the coding conventions stated at the [contributing.md] file. [contributing.md]: https://github.com/jaguililla/hexagonal_spring/blob/main/CONTRIBUTING.md diff --git a/.gitlab/merge_request_templates/mr.md b/.gitlab/merge_request_templates/mr.md index f177803..a420f6f 100644 --- a/.gitlab/merge_request_templates/mr.md +++ b/.gitlab/merge_request_templates/mr.md @@ -9,7 +9,6 @@ For the Pull Request to be accepted please check: - [ ] If the PR refers to an issue, it should be referenced with the GitHub format (*#ID*). - [ ] The PR is done to the `develop` branch (new features) or the `master` branch (releases). - [ ] The code pass all PR checks. -- [ ] All public members are documented. - [ ] The code follow the coding conventions stated at the [contributing.md] file. [contributing.md]: https://github.com/jaguililla/hexagonal_spring/blob/main/CONTRIBUTING.md From 537a32d4724f2a1dcffc26be4d0aa6d22041302c Mon Sep 17 00:00:00 2001 From: jaguililla Date: Wed, 11 Sep 2024 07:52:22 +0200 Subject: [PATCH 3/6] Set next release version and update :documentation --- README.md | 15 ++++++++------- pom.xml | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c7283f7..60fdb45 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Example application to create appointments (REST API). Appointments are stored i ## ๐Ÿงช Test * ArchUnit (preferred over Java modules: it allows naming checks, etc.) * Testcontainers (used to provide a test instance of Postgres and Kafka) +* Pitest (mutation testing, nightly) ## โš’๏ธ Development * SDKMAN (allows to use simpler runners on CI) @@ -69,13 +70,13 @@ Example application to create appointments (REST API). Appointments are stored i ## ๐Ÿ“– Architecture ![Architecture Diagram](doc/architecture.svg) -* **Port:** interface to set a boundary between application logic and implementation details. -* **Adapter:**: port implementation to connect the application domain with the system's context. -* **Domain:**: application logic and model entities. -* **Service:**: implement operations with a common topic altogether. Usually calls driven ports. -* **UseCase/Case:**: single operation service (isolate features). They can coexist with services. -* **Output/Driven Adapter:**: implementation of ports called from the domain. -* **Input/Driver Adapter:**: commands that call application logic (don't require a port). +* **Port**: interface to set a boundary between application logic and implementation details. +* **Adapter**: port implementation to connect the application domain with the system's context. +* **Domain**: application logic and model entities. +* **Service**: implement operations with a common topic altogether. Usually calls driven ports. +* **UseCase/Case**: single operation service (isolate features). They can coexist with services. +* **Output/Driven Adapter**: implementation of ports called from the domain. +* **Input/Driver Adapter**: commands that call application logic (don't require a port). ## ๐Ÿ“š Design * The REST API controller and client are generated from the OpenAPI spec at build time. diff --git a/pom.xml b/pom.xml index 23cd11b..973acc1 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ appointments - 0.3.6 + 0.3.7 Appointments Application to create appointments (REST API) From 844f8432da88e37dd437efe4e8ea932df147dc91 Mon Sep 17 00:00:00 2001 From: jaguililla Date: Wed, 11 Sep 2024 07:53:19 +0200 Subject: [PATCH 4/6] Take advantage of Spring injection for repositories --- .../appointments/ApplicationConfiguration.java | 14 -------------- .../JdbcTemplateAppointmentsRepository.java | 7 +++++-- .../repositories/JdbcTemplateUsersRepository.java | 4 +++- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java b/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java index 25a02b4..3d9735a 100644 --- a/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java +++ b/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java @@ -75,20 +75,6 @@ public KafkaTemplate kafkaTemplate( return kafkaTemplate; } - @Bean - AppointmentsRepository appointmentsStore(final DataSource dataSource) { - final var type = JdbcTemplateAppointmentsRepository.class.getSimpleName(); - LOGGER.info("Creating Appointments Store: {}", type); - return new JdbcTemplateAppointmentsRepository(dataSource); - } - - @Bean - UsersRepository usersStore(final DataSource dataSource) { - final var type = JdbcTemplateUsersRepository.class.getSimpleName(); - LOGGER.info("Creating Users Store: {}", type); - return new JdbcTemplateUsersRepository(dataSource); - } - @Bean AppointmentsNotifier appointmentsNotifier(final KafkaTemplate kafkaTemplate) { final var type = KafkaTemplateAppointmentsNotifier.class.getSimpleName(); diff --git a/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateAppointmentsRepository.java b/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateAppointmentsRepository.java index c598477..ada05f9 100644 --- a/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateAppointmentsRepository.java +++ b/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateAppointmentsRepository.java @@ -14,7 +14,10 @@ import java.sql.SQLException; import java.util.*; import java.util.stream.Stream; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +@Repository public class JdbcTemplateAppointmentsRepository implements AppointmentsRepository { private static final Logger LOGGER = @@ -40,11 +43,11 @@ public JdbcTemplateAppointmentsRepository(final DataSource dataSource) { } @Override + @Transactional public boolean insert(final Appointment appointment) { requireNonNull(appointment, "appointment cannot be null"); LOGGER.debug("--> Creating appointment: {}", appointment); - // TODO Transaction final var parameters = Map.of( "id", appointment.id(), "startTimestamp", appointment.start(), @@ -66,11 +69,11 @@ public boolean insert(final Appointment appointment) { } @Override + @Transactional public boolean delete(final UUID id) { requireNonNull(id, "id cannot be null"); LOGGER.debug("--> Deleting aid: {}", id); - // TODO Transaction final var parameters = Map.of("id", id); final var usersCount = template.update("delete from AppointmentsUsers where appointmentId = :id", parameters); diff --git a/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateUsersRepository.java b/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateUsersRepository.java index 5131c9a..2cccad4 100644 --- a/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateUsersRepository.java +++ b/src/main/java/com/github/jaguililla/appointments/output/repositories/JdbcTemplateUsersRepository.java @@ -15,8 +15,10 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import org.springframework.stereotype.Repository; -public final class JdbcTemplateUsersRepository implements UsersRepository { +@Repository +public class JdbcTemplateUsersRepository implements UsersRepository { private static final Logger LOGGER = LoggerFactory.getLogger(JdbcTemplateUsersRepository.class); From 57920fecea09a27c3896ab4b6a02216eb3e3d144 Mon Sep 17 00:00:00 2001 From: jaguililla Date: Wed, 11 Sep 2024 19:22:38 +0200 Subject: [PATCH 5/6] Improve Kafka messages handling --- .../{mr.md => merge_request_template.md} | 0 .mvn/parent.xml | 2 - README.md | 2 +- .../ApplicationConfiguration.java | 62 ++++++++----------- src/main/resources/application.yml | 7 +++ .../appointments/ApplicationIT.java | 12 ++++ 6 files changed, 45 insertions(+), 40 deletions(-) rename .gitlab/merge_request_templates/{mr.md => merge_request_template.md} (100%) diff --git a/.gitlab/merge_request_templates/mr.md b/.gitlab/merge_request_templates/merge_request_template.md similarity index 100% rename from .gitlab/merge_request_templates/mr.md rename to .gitlab/merge_request_templates/merge_request_template.md diff --git a/.mvn/parent.xml b/.mvn/parent.xml index 73712c1..e859bb9 100644 --- a/.mvn/parent.xml +++ b/.mvn/parent.xml @@ -310,9 +310,7 @@ maven-checkstyle-plugin 3.5.0 - UTF-8 true - false diff --git a/README.md b/README.md index 60fdb45..e5fc028 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Example application to create appointments (REST API). Appointments are stored i * No input ports: they don't need to be decoupled, they just use the domain (and that's acceptable). ## ๐Ÿ“– Architecture -![Architecture Diagram](doc/architecture.svg) +![Architecture Diagram](https://raw.githubusercontent.com/jaguililla/hexagonal_spring/main/doc/architecture.svg) * **Port**: interface to set a boundary between application logic and implementation details. * **Adapter**: port implementation to connect the application domain with the system's context. * **Domain**: application logic and model entities. diff --git a/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java b/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java index 3d9735a..6bc74c1 100644 --- a/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java +++ b/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java @@ -5,30 +5,18 @@ import com.github.jaguililla.appointments.domain.AppointmentsService; import com.github.jaguililla.appointments.domain.UsersRepository; import com.github.jaguililla.appointments.output.notifiers.KafkaTemplateAppointmentsNotifier; -import com.github.jaguililla.appointments.output.repositories.JdbcTemplateAppointmentsRepository; -import com.github.jaguililla.appointments.output.repositories.JdbcTemplateUsersRepository; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.NewTopic; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.core.*; -import javax.sql.DataSource; -import java.util.Map; @Configuration class ApplicationConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationConfiguration.class); - @Value(value = "${spring.kafka.bootstrap-servers}") - private String bootstrapAddress; @Value(value = "${notifierTopic}") private String notifierTopic; @Value(value = "${createMessage}") @@ -36,34 +24,34 @@ class ApplicationConfiguration { @Value(value = "${deleteMessage}") private String deleteMessage; - @Bean - public KafkaAdmin kafkaAdmin() { - return new KafkaAdmin(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress)); - } +// @Bean +// public KafkaAdmin kafkaAdmin() { +// return new KafkaAdmin(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress)); +// } - @Bean - public NewTopic appointmentsTopic() { - return new NewTopic("appointments", 1, (short) 1); - } +// @Bean +// public NewTopic appointmentsTopic() { +// return new NewTopic("appointments", 1, (short) 1); +// } - @Bean - public ProducerFactory producerFactory() { - return new DefaultKafkaProducerFactory<>(Map.of( - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress, - ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class, - ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class - )); - } +// @Bean +// public ProducerFactory producerFactory() { +// return new DefaultKafkaProducerFactory<>(Map.of( +// ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress, +// ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class, +// ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class +// )); +// } - @Bean - public ConsumerFactory consumerFactory() { - return new DefaultKafkaConsumerFactory<>(Map.of( - ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress, - ConsumerConfig.GROUP_ID_CONFIG, "group", - ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class, - ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class - )); - } +// @Bean +// public ConsumerFactory consumerFactory() { +// return new DefaultKafkaConsumerFactory<>(Map.of( +// ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress, +// ConsumerConfig.GROUP_ID_CONFIG, "group", +// ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class, +// ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class +// )); +// } @Bean public KafkaTemplate kafkaTemplate( diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c0e6a93..df322f7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,3 +20,10 @@ spring: kafka: bootstrap-servers: ${KAFKA_SERVER:localhost:9092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer + consumer: + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + group-id: group diff --git a/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java b/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java index 3d2f29f..e8a10f4 100644 --- a/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java +++ b/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java @@ -7,12 +7,15 @@ import com.github.jaguililla.appointments.http.controllers.messages.AppointmentRequest; import com.github.jaguililla.appointments.http.controllers.messages.AppointmentResponse; +import java.time.Duration; +import java.util.List; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @@ -34,6 +37,8 @@ class ApplicationIT { private final TestTemplate client; @Autowired private KafkaTemplate kafkaTemplate; + @Autowired + private ConsumerFactory consumerFactory; ApplicationIT(@LocalServerPort final int portTest) { client = new TestTemplate("http://localhost:" + portTest); @@ -83,6 +88,13 @@ void existing_appointments_can_be_fetched() { @Test void appointments_can_be_created_read_and_deleted() { + try (var consumer = consumerFactory.createConsumer()) { + consumer.subscribe(List.of("appointments")); + for (var r : consumer.poll(Duration.ZERO)) { + + } + } + client.post("/appointments", new AppointmentRequest() .id(UUID.randomUUID()) .startTimestamp(LocalDateTime.now()) From 5afaa07045f11b16c220c8f73d65eb62fd9b6fb1 Mon Sep 17 00:00:00 2001 From: jaguililla Date: Thu, 12 Sep 2024 19:10:28 +0200 Subject: [PATCH 6/6] Improve Kafka messages handling --- .../ApplicationConfiguration.java | 42 +------------------ .../KafkaTemplateAppointmentsNotifier.java | 31 +++++++++----- src/main/resources/application.yml | 4 +- .../appointments/ApplicationIT.java | 27 ++++++------ 4 files changed, 37 insertions(+), 67 deletions(-) diff --git a/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java b/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java index 6bc74c1..d26ec11 100644 --- a/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java +++ b/src/main/java/com/github/jaguililla/appointments/ApplicationConfiguration.java @@ -24,45 +24,6 @@ class ApplicationConfiguration { @Value(value = "${deleteMessage}") private String deleteMessage; -// @Bean -// public KafkaAdmin kafkaAdmin() { -// return new KafkaAdmin(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress)); -// } - -// @Bean -// public NewTopic appointmentsTopic() { -// return new NewTopic("appointments", 1, (short) 1); -// } - -// @Bean -// public ProducerFactory producerFactory() { -// return new DefaultKafkaProducerFactory<>(Map.of( -// ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress, -// ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class, -// ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class -// )); -// } - -// @Bean -// public ConsumerFactory consumerFactory() { -// return new DefaultKafkaConsumerFactory<>(Map.of( -// ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress, -// ConsumerConfig.GROUP_ID_CONFIG, "group", -// ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class, -// ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class -// )); -// } - - @Bean - public KafkaTemplate kafkaTemplate( - final ProducerFactory producerFactory, - final ConsumerFactory consumerFactory - ) { - final var kafkaTemplate = new KafkaTemplate<>(producerFactory); - kafkaTemplate.setConsumerFactory(consumerFactory); - return kafkaTemplate; - } - @Bean AppointmentsNotifier appointmentsNotifier(final KafkaTemplate kafkaTemplate) { final var type = KafkaTemplateAppointmentsNotifier.class.getSimpleName(); @@ -78,6 +39,7 @@ AppointmentsService appointmentsService( final UsersRepository usersRepository, final AppointmentsNotifier appointmentsNotifier ) { - return new AppointmentsService(appointmentsRepository, usersRepository, appointmentsNotifier); + return + new AppointmentsService(appointmentsRepository, usersRepository, appointmentsNotifier); } } diff --git a/src/main/java/com/github/jaguililla/appointments/output/notifiers/KafkaTemplateAppointmentsNotifier.java b/src/main/java/com/github/jaguililla/appointments/output/notifiers/KafkaTemplateAppointmentsNotifier.java index 06c2dd4..0e0d0a9 100644 --- a/src/main/java/com/github/jaguililla/appointments/output/notifiers/KafkaTemplateAppointmentsNotifier.java +++ b/src/main/java/com/github/jaguililla/appointments/output/notifiers/KafkaTemplateAppointmentsNotifier.java @@ -3,6 +3,7 @@ import com.github.jaguililla.appointments.domain.AppointmentsNotifier; import com.github.jaguililla.appointments.domain.Event; import com.github.jaguililla.appointments.domain.model.Appointment; +import java.util.concurrent.ExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.kafka.core.KafkaTemplate; @@ -33,16 +34,24 @@ public KafkaTemplateAppointmentsNotifier( public void notify(final Event event, final Appointment appointment) { final var message = event == Event.CREATED ? createMessage : deleteMessage; - kafkaTemplate - .send(notifierTopic, message.formatted(appointment.start())) - .whenComplete((result, e) -> { - if (e == null) { - final var metadata = result.getRecordMetadata(); - LOGGER.info("Message: '{}' offset: {}", message, metadata.offset()); - } - else { - LOGGER.info("Message: '{}' FAILED due to: {}", message, e.getMessage()); - } - }); + try { + kafkaTemplate + .send(notifierTopic, message.formatted(appointment.start())) + .whenComplete((result, e) -> { + if (e == null) { + final var metadata = result.getRecordMetadata(); + LOGGER.info("Message: '{}' offset: {}", message, metadata.offset()); + } + else { + LOGGER.info("Message: '{}' FAILED due to: {}", message, e.getMessage()); + } + }) + .get(); + } + catch (InterruptedException | ExecutionException e) { + var id = appointment.id(); + var errorMessage = "Error sending notification for appointment: %s".formatted(id); + throw new IllegalStateException(errorMessage, e); + } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index df322f7..ef32648 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,7 +23,9 @@ spring: producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer + # Consumer is only used in tests, auto-offset-reset is *REQUIRED* consumer: key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer - group-id: group + group-id: tests + auto-offset-reset: earliest diff --git a/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java b/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java index e8a10f4..a531975 100644 --- a/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java +++ b/src/test/java/com/github/jaguililla/appointments/ApplicationIT.java @@ -1,6 +1,5 @@ package com.github.jaguililla.appointments; -import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @@ -9,6 +8,7 @@ import com.github.jaguililla.appointments.http.controllers.messages.AppointmentResponse; import java.time.Duration; import java.util.List; +import org.apache.kafka.common.TopicPartition; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -16,7 +16,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.KafkaTemplate; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.PostgreSQLContainer; @@ -36,8 +35,6 @@ class ApplicationIT { private final TestTemplate client; @Autowired - private KafkaTemplate kafkaTemplate; - @Autowired private ConsumerFactory consumerFactory; ApplicationIT(@LocalServerPort final int portTest) { @@ -88,13 +85,6 @@ void existing_appointments_can_be_fetched() { @Test void appointments_can_be_created_read_and_deleted() { - try (var consumer = consumerFactory.createConsumer()) { - consumer.subscribe(List.of("appointments")); - for (var r : consumer.poll(Duration.ZERO)) { - - } - } - client.post("/appointments", new AppointmentRequest() .id(UUID.randomUUID()) .startTimestamp(LocalDateTime.now()) @@ -102,15 +92,22 @@ void appointments_can_be_created_read_and_deleted() { ); var response = client.getResponseBody(AppointmentResponse.class); assertEquals(200, client.getResponseStatus().value()); - var creationMessage = requireNonNull(kafkaTemplate.receive("appointments", 0, 0)); - assertTrue(creationMessage.value().startsWith("Appointment created at")); + assertTrue(getLastMessage().startsWith("Appointment created at")); client.get("/appointments/" + response.getId()); assertEquals(200, client.getResponseStatus().value()); client.delete("/appointments/" + response.getId()); assertEquals(200, client.getResponseStatus().value()); - var deletionMessage = requireNonNull(kafkaTemplate.receive("appointments", 0, 1)); - assertTrue(deletionMessage.value().startsWith("Appointment deleted at")); + assertTrue(getLastMessage().startsWith("Appointment deleted at")); client.delete("/appointments/" + response.getId()); assertEquals(404, client.getResponseStatus().value()); } + + private String getLastMessage() { + try (var consumer = consumerFactory.createConsumer()) { + consumer.assign(List.of(new TopicPartition("appointments", 0))); + var record = consumer.poll(Duration.ofMillis(250)).iterator().next().value(); + consumer.commitSync(); + return record; + } + } }