Skip to content

Commit 649b721

Browse files
committed
Some unit tests
1 parent 68cf247 commit 649b721

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

zuul-core/src/test/java/com/netflix/netty/common/HttpClientLifecycleChannelHandlerTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,23 @@
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
2020

21+
import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteEvent;
22+
import com.netflix.netty.common.HttpLifecycleChannelHandler.CompleteReason;
23+
import com.netflix.netty.common.HttpLifecycleChannelHandler.State;
2124
import io.netty.buffer.Unpooled;
25+
import io.netty.channel.ChannelHandlerContext;
26+
import io.netty.channel.ChannelInboundHandlerAdapter;
2227
import io.netty.channel.embedded.EmbeddedChannel;
2328
import io.netty.handler.codec.http.DefaultHttpContent;
2429
import io.netty.handler.codec.http.DefaultHttpRequest;
30+
import io.netty.handler.codec.http.DefaultHttpResponse;
2531
import io.netty.handler.codec.http.DefaultLastHttpContent;
2632
import io.netty.handler.codec.http.HttpMethod;
33+
import io.netty.handler.codec.http.HttpResponseStatus;
2734
import io.netty.handler.codec.http.HttpVersion;
35+
import io.netty.handler.codec.http.LastHttpContent;
36+
import java.util.ArrayList;
37+
import java.util.List;
2838
import org.junit.jupiter.api.AfterEach;
2939
import org.junit.jupiter.api.BeforeEach;
3040
import org.junit.jupiter.api.Test;
@@ -33,6 +43,19 @@ class HttpClientLifecycleChannelHandlerTest {
3343

3444
private EmbeddedChannel channel;
3545

46+
/** Collects all CompleteEvents fired as user events on the pipeline. */
47+
private static final class CompleteEventCollector extends ChannelInboundHandlerAdapter {
48+
final List<CompleteEvent> events = new ArrayList<>();
49+
50+
@Override
51+
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
52+
if (evt instanceof CompleteEvent ce) {
53+
events.add(ce);
54+
}
55+
super.userEventTriggered(ctx, evt);
56+
}
57+
}
58+
3659
@BeforeEach
3760
void setup() {
3861
channel = new EmbeddedChannel(HttpClientLifecycleChannelHandler.OUTBOUND_CHANNEL_HANDLER);
@@ -64,6 +87,56 @@ void lastContentPendingClearedAfterLastHttpContent() {
6487
.isNull();
6588
}
6689

90+
@Test
91+
void sessionCompleteNotFiredAfter1xxResponse() {
92+
CompleteEventCollector collector = new CompleteEventCollector();
93+
EmbeddedChannel inboundChannel = new EmbeddedChannel(
94+
HttpClientLifecycleChannelHandler.INBOUND_CHANNEL_HANDLER, collector);
95+
inboundChannel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED);
96+
97+
// Netty decodes a 1xx from the origin as two separate events
98+
inboundChannel.writeInbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS));
99+
inboundChannel.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT);
100+
101+
assertThat(collector.events).isEmpty();
102+
inboundChannel.finishAndReleaseAll();
103+
}
104+
105+
@Test
106+
void sessionCompleteFiredAfterFinalResponseFollowing1xx() {
107+
CompleteEventCollector collector = new CompleteEventCollector();
108+
EmbeddedChannel inboundChannel = new EmbeddedChannel(
109+
HttpClientLifecycleChannelHandler.INBOUND_CHANNEL_HANDLER, collector);
110+
inboundChannel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED);
111+
112+
// 1xx pair — must NOT trigger SESSION_COMPLETE
113+
inboundChannel.writeInbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS));
114+
inboundChannel.writeInbound(LastHttpContent.EMPTY_LAST_CONTENT);
115+
assertThat(collector.events).isEmpty();
116+
117+
// Final 200 response — must trigger SESSION_COMPLETE exactly once
118+
inboundChannel.writeInbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK));
119+
inboundChannel.writeInbound(new DefaultLastHttpContent());
120+
assertThat(collector.events).hasSize(1);
121+
assertThat(collector.events.getFirst().getReason()).isEqualTo(CompleteReason.SESSION_COMPLETE);
122+
inboundChannel.finishAndReleaseAll();
123+
}
124+
125+
@Test
126+
void sessionCompleteFiredForNormalFinalResponse() {
127+
CompleteEventCollector collector = new CompleteEventCollector();
128+
EmbeddedChannel inboundChannel = new EmbeddedChannel(
129+
HttpClientLifecycleChannelHandler.INBOUND_CHANNEL_HANDLER, collector);
130+
inboundChannel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED);
131+
132+
inboundChannel.writeInbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK));
133+
inboundChannel.writeInbound(new DefaultLastHttpContent());
134+
135+
assertThat(collector.events).hasSize(1);
136+
assertThat(collector.events.getFirst().getReason()).isEqualTo(CompleteReason.SESSION_COMPLETE);
137+
inboundChannel.finishAndReleaseAll();
138+
}
139+
67140
@Test
68141
void lastContentPendingResetsOnNewRequest() {
69142
channel.writeOutbound(new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/first"));

zuul-core/src/test/java/com/netflix/netty/common/HttpServerLifecycleChannelHandlerTest.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,39 @@
2424
import com.netflix.netty.common.HttpServerLifecycleChannelHandler.HttpServerLifecycleInboundChannelHandler;
2525
import com.netflix.netty.common.HttpServerLifecycleChannelHandler.HttpServerLifecycleOutboundChannelHandler;
2626
import io.netty.buffer.ByteBuf;
27+
import io.netty.buffer.Unpooled;
2728
import io.netty.buffer.UnpooledByteBufAllocator;
2829
import io.netty.channel.ChannelHandlerContext;
2930
import io.netty.channel.ChannelInboundHandlerAdapter;
3031
import io.netty.channel.embedded.EmbeddedChannel;
3132
import io.netty.handler.codec.http.DefaultFullHttpRequest;
33+
import io.netty.handler.codec.http.DefaultFullHttpResponse;
34+
import io.netty.handler.codec.http.DefaultHttpResponse;
35+
import io.netty.handler.codec.http.DefaultLastHttpContent;
3236
import io.netty.handler.codec.http.FullHttpRequest;
3337
import io.netty.handler.codec.http.HttpMethod;
38+
import io.netty.handler.codec.http.HttpResponseStatus;
3439
import io.netty.handler.codec.http.HttpVersion;
40+
import io.netty.handler.codec.http.LastHttpContent;
3541
import io.netty.util.ReferenceCountUtil;
42+
import java.util.ArrayList;
43+
import java.util.List;
3644
import org.junit.jupiter.api.Test;
3745

3846
class HttpServerLifecycleChannelHandlerTest {
3947

48+
private static final class CompleteEventCollector extends ChannelInboundHandlerAdapter {
49+
final List<CompleteEvent> events = new ArrayList<>();
50+
51+
@Override
52+
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
53+
if (evt instanceof CompleteEvent ce) {
54+
events.add(ce);
55+
}
56+
super.userEventTriggered(ctx, evt);
57+
}
58+
}
59+
4060
final class AssertReasonHandler extends ChannelInboundHandlerAdapter {
4161

4262
CompleteEvent completeEvent;
@@ -82,6 +102,68 @@ void completionEventReasonIsCloseByDefault() {
82102
assertThat(reasonHandler.getCompleteEvent().getReason()).isEqualTo(CompleteReason.CLOSE);
83103
}
84104

105+
@Test
106+
void sessionCompleteNotFiredAfterForwarded1xxAsTwoMessages() {
107+
// A 1xx forwarded from origin arrives as two separate pipeline objects:
108+
// an HttpResponse(103) and a trailing empty LastHttpContent.
109+
CompleteEventCollector collector = new CompleteEventCollector();
110+
EmbeddedChannel channel = new EmbeddedChannel(new HttpServerLifecycleOutboundChannelHandler(), collector);
111+
channel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED);
112+
113+
channel.writeOutbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS));
114+
channel.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT);
115+
116+
assertThat(collector.events).isEmpty();
117+
channel.finishAndReleaseAll();
118+
}
119+
120+
@Test
121+
void sessionCompleteNotFiredAfterFullHttpResponse100() {
122+
// Zuul-generated 100 Continue arrives as a FullHttpResponse (both HttpResponse and LastHttpContent).
123+
CompleteEventCollector collector = new CompleteEventCollector();
124+
EmbeddedChannel channel = new EmbeddedChannel(new HttpServerLifecycleOutboundChannelHandler(), collector);
125+
channel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED);
126+
127+
channel.writeOutbound(
128+
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE, Unpooled.EMPTY_BUFFER));
129+
130+
assertThat(collector.events).isEmpty();
131+
channel.finishAndReleaseAll();
132+
}
133+
134+
@Test
135+
void sessionCompleteFiredAfterFinalResponseFollowing1xx() {
136+
CompleteEventCollector collector = new CompleteEventCollector();
137+
EmbeddedChannel channel = new EmbeddedChannel(new HttpServerLifecycleOutboundChannelHandler(), collector);
138+
channel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED);
139+
140+
// Interim 1xx pair — must NOT fire SESSION_COMPLETE
141+
channel.writeOutbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS));
142+
channel.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT);
143+
assertThat(collector.events).isEmpty();
144+
145+
// Final 200 — must fire SESSION_COMPLETE exactly once
146+
channel.writeOutbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK));
147+
channel.writeOutbound(new DefaultLastHttpContent());
148+
assertThat(collector.events).hasSize(1);
149+
assertThat(collector.events.get(0).getReason()).isEqualTo(CompleteReason.SESSION_COMPLETE);
150+
channel.finishAndReleaseAll();
151+
}
152+
153+
@Test
154+
void sessionCompleteFiredForNormalFinalResponse() {
155+
CompleteEventCollector collector = new CompleteEventCollector();
156+
EmbeddedChannel channel = new EmbeddedChannel(new HttpServerLifecycleOutboundChannelHandler(), collector);
157+
channel.attr(HttpLifecycleChannelHandler.ATTR_STATE).set(State.STARTED);
158+
159+
channel.writeOutbound(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK));
160+
channel.writeOutbound(new DefaultLastHttpContent());
161+
162+
assertThat(collector.events).hasSize(1);
163+
assertThat(collector.events.get(0).getReason()).isEqualTo(CompleteReason.SESSION_COMPLETE);
164+
channel.finishAndReleaseAll();
165+
}
166+
85167
@Test
86168
void pipelineRejectReleasesIfNeeded() {
87169

zuul-core/src/test/java/com/netflix/zuul/filters/endpoint/ProxyEndpointTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,47 @@ private static DiscoveryResult createDiscoveryResult() {
368368
return DiscoveryResult.from(instanceInfo, true);
369369
}
370370

371+
// --- 1xx interim response tests ---
372+
373+
@Test
374+
void interimResponseIsForwardedWithoutSettingStartedSendingFlag() {
375+
proxyEndpoint.responseFromOrigin(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS));
376+
377+
// invokeNext must be called to forward the 103 to the downstream client
378+
verify(proxyEndpoint).invokeNext(any(HttpResponseMessage.class));
379+
// startedSendingResponseToClient must remain false so that retries remain possible
380+
assertThat(proxyEndpoint.startedSendingResponseToClient).isFalse();
381+
}
382+
383+
@Test
384+
void finalResponseIsForwardedAfterInterimResponse() {
385+
// 1xx from origin: HttpResponse(103) then empty LastHttpContent
386+
proxyEndpoint.responseFromOrigin(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS));
387+
proxyEndpoint.invokeNext(LastHttpContent.EMPTY_LAST_CONTENT);
388+
389+
// Real 200 arrives next — must still be routed correctly
390+
proxyEndpoint.responseFromOrigin(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK));
391+
392+
// invokeNext(HttpResponseMessage) must have been called twice: once for 103, once for 200
393+
verify(proxyEndpoint, times(2)).invokeNext(any(HttpResponseMessage.class));
394+
assertThat(proxyEndpoint.startedSendingResponseToClient).isTrue();
395+
}
396+
397+
@Test
398+
void multiple1xxResponsesBeforeFinalResponseAreAllForwarded() {
399+
proxyEndpoint.responseFromOrigin(
400+
new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.PROCESSING));
401+
proxyEndpoint.invokeNext(LastHttpContent.EMPTY_LAST_CONTENT);
402+
403+
proxyEndpoint.responseFromOrigin(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.EARLY_HINTS));
404+
proxyEndpoint.invokeNext(LastHttpContent.EMPTY_LAST_CONTENT);
405+
406+
proxyEndpoint.responseFromOrigin(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK));
407+
408+
verify(proxyEndpoint, times(3)).invokeNext(any(HttpResponseMessage.class));
409+
assertThat(proxyEndpoint.startedSendingResponseToClient).isTrue();
410+
}
411+
371412
private void createResponse(HttpResponseStatus status) {
372413
response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
373414
}

0 commit comments

Comments
 (0)