/*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package feign;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.api.SoftAssertions;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runners.model.Statement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import feign.Logger.Level;
@RunWith(Enclosed.class)
public class LoggerTest {
@Rule
public final MockWebServer server = new MockWebServer();
@Rule
public final RecordingLogger logger = new RecordingLogger();
@Rule
public final ExpectedException thrown = ExpectedException.none();
interface SendsStuff {
@RequestLine("POST /")
@Headers("Content-Type: application/json")
@Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
String login(
@Param("customer_name") String customer,
@Param("user_name") String user, @Param("password") String password);
}
@RunWith(Parameterized.class)
public static class LogLevelEmitsTest extends LoggerTest {
private final Level logLevel;
public LogLevelEmitsTest(Level logLevel, List<String> expectedMessages) {
this.logLevel = logLevel;
logger.expectMessages(expectedMessages);
}
@Parameters
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{Level.NONE, Arrays.asList()},
{Level.BASIC, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)")},
{Level.HEADERS, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] content-length: 3",
"\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)")},
{Level.FULL, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] \\{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"\\}",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] content-length: 3",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] foo",
"\\[SendsStuff#login\\] <--- END HTTP \\(3-byte body\\)")}
});
}
@Test
public void levelEmits() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setBody("foo"));
SendsStuff api = Feign.builder()
.logger(logger)
.logLevel(logLevel)
.target(SendsStuff.class, "http://localhost:" + server.getPort());
api.login("netflix", "denominator", "password");
}
}
@RunWith(Parameterized.class)
public static class ReasonPhraseOptional extends LoggerTest {
private final Level logLevel;
public ReasonPhraseOptional(Level logLevel, List<String> expectedMessages) {
this.logLevel = logLevel;
logger.expectMessages(expectedMessages);
}
@Parameters
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{Level.BASIC, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 \\([0-9]+ms\\)")},
});
}
@Test
public void reasonPhraseOptional() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setStatus("HTTP/1.1 " + 200));
SendsStuff api = Feign.builder()
.logger(logger)
.logLevel(logLevel)
.target(SendsStuff.class, "http://localhost:" + server.getPort());
api.login("netflix", "denominator", "password");
}
}
@RunWith(Parameterized.class)
public static class ReadTimeoutEmitsTest extends LoggerTest {
private final Level logLevel;
public ReadTimeoutEmitsTest(Level logLevel, List<String> expectedMessages) {
this.logLevel = logLevel;
logger.expectMessages(expectedMessages);
}
@Parameters
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{Level.NONE, Arrays.asList()},
{Level.BASIC, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)")},
{Level.HEADERS, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] content-length: 3",
"\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)")},
{Level.FULL, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://localhost:[0-9]+/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] \\{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"\\}",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- HTTP/1.1 200 OK \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] content-length: 3",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] <--- ERROR SocketTimeoutException: Read timed out \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] java.net.SocketTimeoutException: Read timed out.*",
"\\[SendsStuff#login\\] <--- END ERROR")}
});
}
@Test
public void levelEmitsOnReadTimeout() throws IOException, InterruptedException {
server.enqueue(new MockResponse().throttleBody(1, 1, TimeUnit.SECONDS).setBody("foo"));
thrown.expect(FeignException.class);
SendsStuff api = Feign.builder()
.logger(logger)
.logLevel(logLevel)
.options(new Request.Options(10 * 1000, 50))
.target(SendsStuff.class, "http://localhost:" + server.getPort());
api.login("netflix", "denominator", "password");
}
}
@RunWith(Parameterized.class)
public static class UnknownHostEmitsTest extends LoggerTest {
private final Level logLevel;
public UnknownHostEmitsTest(Level logLevel, List<String> expectedMessages) {
this.logLevel = logLevel;
logger.expectMessages(expectedMessages);
}
@Parameters
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{Level.NONE, Arrays.asList()},
{Level.BASIC, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)")},
{Level.HEADERS, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)")},
{Level.FULL, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] Content-Type: application/json",
"\\[SendsStuff#login\\] Content-Length: 80",
"\\[SendsStuff#login\\] ",
"\\[SendsStuff#login\\] \\{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"\\}",
"\\[SendsStuff#login\\] ---> END HTTP \\(80-byte body\\)",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] java.net.UnknownHostException: robofu.abc.*",
"\\[SendsStuff#login\\] <--- END ERROR")}
});
}
@Test
public void unknownHostEmits() throws IOException, InterruptedException {
SendsStuff api = Feign.builder()
.logger(logger)
.logLevel(logLevel)
.retryer(new Retryer() {
@Override
public void continueOrPropagate(RetryableException e) {
throw e;
}
@Override public Retryer clone() {
return this;
}
})
.target(SendsStuff.class, "http://robofu.abc");
thrown.expect(FeignException.class);
api.login("netflix", "denominator", "password");
}
}
@RunWith(Parameterized.class)
public static class RetryEmitsTest extends LoggerTest {
private final Level logLevel;
public RetryEmitsTest(Level logLevel, List<String> expectedMessages) {
this.logLevel = logLevel;
logger.expectMessages(expectedMessages);
}
@Parameters
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
{Level.NONE, Arrays.asList()},
{Level.BASIC, Arrays.asList(
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)",
"\\[SendsStuff#login\\] ---> RETRYING",
"\\[SendsStuff#login\\] ---> POST http://robofu.abc/ HTTP/1.1",
"\\[SendsStuff#login\\] <--- ERROR UnknownHostException: robofu.abc \\([0-9]+ms\\)")}
});
}
@Test
public void retryEmits() throws IOException, InterruptedException {
thrown.expect(FeignException.class);
SendsStuff api = Feign.builder()
.logger(logger)
.logLevel(logLevel)
.retryer(new Retryer() {
boolean retried;
@Override
public void continueOrPropagate(RetryableException e) {
if (!retried) {
retried = true;
return;
}
throw e;
}
@Override
public Retryer clone() {
return this;
}
})
.target(SendsStuff.class, "http://robofu.abc");
api.login("netflix", "denominator", "password");
}
}
private static final class RecordingLogger extends Logger implements TestRule {
private final List<String> messages = new ArrayList<String>();
private final List<String> expectedMessages = new ArrayList<String>();
RecordingLogger expectMessages(List<String> expectedMessages) {
this.expectedMessages.addAll(expectedMessages);
return this;
}
@Override
protected void log(String configKey, String format, Object... args) {
messages.add(methodTag(configKey) + String.format(format, args));
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
SoftAssertions softly = new SoftAssertions();
for (int i = 0; i < messages.size(); i++) {
softly.assertThat(messages.get(i)).matches(expectedMessages.get(i));
}
softly.assertAll();
}
};
}
}
}