Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package de.schulung.quarkus.recipes.domain;

import de.schulung.quarkus.recipes.domain.events.RecipeCreatedEvent;
import de.schulung.quarkus.recipes.domain.events.RecipeEvent;
import de.schulung.quarkus.recipes.domain.model.Difficulty;
import de.schulung.quarkus.recipes.domain.model.Recipe;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

Expand All @@ -15,6 +18,7 @@
public class RecipesService {

private final RecipesDao dao;
private final Event<RecipeEvent> eventPublisher;

public Stream<Recipe> findAll() {
return dao.findAll();
Expand All @@ -39,5 +43,7 @@ public void create(
}

dao.save(recipe);

eventPublisher.fire(new RecipeCreatedEvent(recipe));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.schulung.quarkus.recipes.domain.events;

import de.schulung.quarkus.recipes.domain.model.Recipe;

public record RecipeCreatedEvent(
Recipe recipe
) implements RecipeEvent {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package de.schulung.quarkus.recipes.domain.events;

public interface RecipeEvent {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.schulung.quarkus.recipes.infrastructure;

import de.schulung.quarkus.recipes.domain.events.RecipeCreatedEvent;
import io.quarkus.arc.log.LoggerName;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import org.jboss.logging.Logger;

@ApplicationScoped
public class RecipeEventsLogger {

@LoggerName("customer-events")
Logger log;

public void logRecipeCreatedEvent(
@Observes
RecipeCreatedEvent event
) {
log.infov(
"Recipe created with id={0}",
event.recipe().getId()
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package de.schulung.quarkus.recipes.infrastructure;

import de.schulung.quarkus.recipes.test.CaptureOutput;
import de.schulung.quarkus.recipes.test.CaptureOutput.CapturedOutput;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertTrue;

@QuarkusTest
@TestTransaction
@CaptureOutput
class RecipeEventsLoggerTests {

@Test
void whenCreateRecipe_thenLogEvent(CapturedOutput output) {
var response = given()
.contentType(ContentType.JSON)
.body("""
{
"name": "Tomato Soup",
"img": "/my-image",
"servings": 4,
"duration": 30,
"difficulty": "easy",
"ingredients": [
{ "unit": "pieces", "quantity": 2.0, "name": "Tomato" }
],
"preparation": "Boil tomatoes."
}
""")
.accept(ContentType.JSON)
.when()
.post("/recipes");
response
.then()
.statusCode(201)
.header("Location", is(notNullValue()));
var id = response.body().jsonPath().getString("id");

assertTrue(
output.toString().matches("(?si).*Recipe created.*" + id + ".*"),
"Expected log output to contain 'Recipe created' with id=" + id
);
}

}
141 changes: 141 additions & 0 deletions src/test/java/de/schulung/quarkus/recipes/test/CaptureOutput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package de.schulung.quarkus.recipes.test;

import org.jspecify.annotations.NonNull;
import org.junit.jupiter.api.extension.*;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.text.MessageFormat;
import java.util.function.Supplier;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

/**
* Annotation to enable capturing of output produced during test execution.
* <p>
* When applied to a test method or class, any output (such as log messages) produced
* during the test's execution is captured, allowing it to be accessed and verified.
* <p>
* This annotation works in conjunction with the {@link OutputCaptureExtension}, which
* handles the logic for capturing and providing access to the captured output.
* <p>
* The annotation can be applied at the test method or class level.
* <p>
* Target: {@link ElementType#METHOD}, {@link ElementType#TYPE}
* Retention: {@link RetentionPolicy#RUNTIME}
*
* @see OutputCaptureExtension
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD,
ElementType.TYPE
})
@ExtendWith(CaptureOutput.OutputCaptureExtension.class)
public @interface CaptureOutput {

@SuppressWarnings("NullableProblems")
public record CapturedOutput(Supplier<String> output)
implements CharSequence {

@Override
public int length() {
return output.get().length();
}

@Override
public char charAt(int index) {
return output.get().charAt(index);
}

@Override
public CharSequence subSequence(int start, int end) {
return output.get().subSequence(start, end);
}

@Override
public String toString() {
return output.get();
}
}

class OutputCaptureExtension
implements BeforeEachCallback, AfterEachCallback, ParameterResolver {

private Handler handler;
private CapturedOutput capturedOutput;

@Override
public void beforeEach(
@NonNull
ExtensionContext context
) {

final StringBuilder sb = new StringBuilder();
this.handler = new Handler() {

@Override
public void publish(LogRecord record) {
final var message = MessageFormat
.format(
record.getMessage(),
record.getParameters()
);
sb.append(message);
}

@Override
public void flush() {
}

@Override
public void close() throws SecurityException {
}
};
this.capturedOutput = new CapturedOutput(sb::toString);

Logger
.getLogger("")
.addHandler(this.handler);
}

@Override
public void afterEach(
@NonNull
ExtensionContext context
) {
Logger
.getLogger("")
.removeHandler(this.handler);
this.handler = null;
this.capturedOutput = null;
}

@Override
public boolean supportsParameter(
ParameterContext parameterContext,
@NonNull
ExtensionContext context
) {
return parameterContext
.getParameter()
.getType()
.equals(CapturedOutput.class);
}

@Override
public Object resolveParameter(
@NonNull
ParameterContext parameterContext,
@NonNull
ExtensionContext context
) {
return this.capturedOutput;
}

}

}