io.kroxylicious.test.server.MockHandler Maven / Gradle / Ivy
/*
* Copyright Kroxylicious Authors.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.kroxylicious.test.server;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import org.apache.kafka.common.protocol.ApiKeys;
import org.apache.kafka.common.protocol.ApiMessage;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
import org.hamcrest.TypeSafeMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.kroxylicious.test.Request;
import io.kroxylicious.test.codec.DecodedRequestFrame;
/**
* MockHandler is responsible for:
*
* - Serves a single response for any requests it receives. The response can be modified
* * using setResponse.
* - Records requests it receives so they can be retrieved and verified
* - Can be cleared, making it forget received requests
*
*/
@Sharable
public class MockHandler extends ChannelInboundHandlerAdapter {
private record ConditionalMockResponse(Matcher matcher, Action action, AtomicLong invocations) {
}
private static final Logger logger = LoggerFactory.getLogger(MockHandler.class);
private final List conditionalMockResponses = new ArrayList<>();
private final List> requests = new ArrayList<>();
/**
* Create mockhandler with initial message to serve
* @param message message to respond with, nullable
*/
public MockHandler(ApiMessage message) {
if (message != null) {
setMockResponseForApiKey(ApiKeys.forId(message.apiKey()), message);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
DecodedRequestFrame> msg1 = (DecodedRequestFrame>) msg;
respond(ctx, msg1);
}
private void respond(ChannelHandlerContext ctx, DecodedRequestFrame> frame) {
requests.add(frame);
ConditionalMockResponse response = conditionalMockResponses.stream().filter(r -> r.matcher.matches(MockServer.toRequest(frame))).findFirst().orElseThrow();
response.action.handle(ctx, frame);
response.invocations.incrementAndGet();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error("Exception in Mock Handler", cause);
// Close the connection when an exception is raised.
ctx.close();
}
/**
* Set the response
* @param response response
*/
public void setMockResponseForApiKey(ApiKeys keys, ApiMessage response) {
addMockResponse(new TypeSafeMatcher<>() {
@Override
protected boolean matchesSafely(Request request) {
return request.apiKeys() == keys;
}
@Override
public void describeTo(Description description) {
description.appendText("has key " + keys);
}
}, Action.respond(response));
}
public void addMockResponse(Matcher matcher, Action action) {
conditionalMockResponses.add(new ConditionalMockResponse(matcher, action, new AtomicLong(0)));
}
/**
* Get requests
* @return get received requests
*/
public List> getRequests() {
return Collections.unmodifiableList(requests);
}
public void assertAllMockInteractionsInvoked() {
List anyUninvoked = conditionalMockResponses.stream().filter(r -> r.invocations.get() <= 0).toList();
if (!anyUninvoked.isEmpty()) {
String collect = anyUninvoked.stream().map(conditionalMockResponse -> {
StringDescription stringDescription = new StringDescription();
conditionalMockResponse.matcher.describeTo(stringDescription);
return "mock response was never invoked: " + stringDescription;
}).collect(Collectors.joining(","));
throw new AssertionError(collect);
}
}
/**
* Clear recorded requests
*/
public void clear() {
requests.clear();
conditionalMockResponses.clear();
}
}