/*
* Copyright 2015 GoDataDriven B.V.
*
* 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 io.divolte.server;
import avro.shaded.com.google.common.collect.ImmutableMap;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ContainerNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.divolte.server.ServerTestUtils.TestServer;
import io.divolte.server.config.JsonSourceConfiguration;
import org.junit.After;
import org.junit.Assume;
import org.junit.Test;
import javax.annotation.ParametersAreNonnullByDefault;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import static java.net.HttpURLConnection.*;
import static org.junit.Assert.*;
@ParametersAreNonnullByDefault
public class JsonSourceTest {
private static final String JSON_EVENT_WITHOUT_PARTYID_URL_TEMPLATE = "http://%s:%d/json-event";
private static final String JSON_EVENT_URL_TEMPLATE = JSON_EVENT_WITHOUT_PARTYID_URL_TEMPLATE + "?p=0%%3Ai1t84hgy%%3A5AF359Zjq5kUy98u4wQjlIZzWGhN~GlG";
private static final String JSON_EVENT_WITH_BROKEN_PARTYID_URL_TEMPLATE = JSON_EVENT_WITHOUT_PARTYID_URL_TEMPLATE + "?p=notavalidpartyid";
private static final int JSON_MAXIMUM_BODY_SIZE = Integer.parseInt(JsonSourceConfiguration.DEFAULT_MAXIMUM_BODY_SIZE);
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final ObjectNode EMPTY_JSON_OBJECT = JSON_MAPPER.createObjectNode();
private Optional<TestServer> testServer = Optional.empty();
private Optional<String> urlTemplate = Optional.empty();
private void startServer() {
startServer(Collections.emptyMap());
}
private void startServer(final Map<String,Object> extraConfig) {
stopServer();
testServer = Optional.of(new TestServer("json-source.conf", extraConfig));
urlTemplate = Optional.of(JSON_EVENT_URL_TEMPLATE);
}
private void stopServer() {
testServer.ifPresent(TestServer::shutdown);
testServer = Optional.empty();
urlTemplate = Optional.empty();
}
@After
public void tearDown() {
stopServer();
}
private HttpURLConnection request() throws IOException {
return request(EMPTY_JSON_OBJECT);
}
private static <T> Consumer<T> noop() {
return ignored -> {};
}
private HttpURLConnection request(final ContainerNode json) throws IOException {
return request(json, noop());
}
private HttpURLConnection request(final Consumer<HttpURLConnection> preRequest) throws IOException {
return request(JSON_MAPPER.createObjectNode(), preRequest);
}
private HttpURLConnection startRequest() throws IOException {
final TestServer testServer = this.testServer.orElseThrow(() -> new IllegalStateException("No test server available"));
final String url = String.format(urlTemplate.orElseThrow(() -> new IllegalStateException("No URL template available")),
testServer.host, testServer.port);
return (HttpURLConnection) new URL(url).openConnection();
}
private HttpURLConnection startJsonPostRequest() throws IOException {
final HttpURLConnection conn = startRequest();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
return conn;
}
private HttpURLConnection request(final ContainerNode json,
final Consumer<HttpURLConnection> preRequest) throws IOException {
final HttpURLConnection conn = startJsonPostRequest();
preRequest.accept(conn);
try (final OutputStream requestBody = conn.getOutputStream()) {
JSON_MAPPER.writeValue(requestBody, json);
}
// This is the canonical way to wait for the server to respond.
conn.getResponseCode();
return conn;
}
@Test
public void shouldSupportMobileSource() {
startServer();
}
@Test
public void shouldSupportPostingJsonToEndpoint() throws IOException {
startServer();
final HttpURLConnection conn = request();
assertEquals(HTTP_NO_CONTENT, conn.getResponseCode());
}
@Test
public void shouldOnlySupportPostRequests() throws IOException {
startServer();
final HttpURLConnection conn = startRequest();
conn.setRequestMethod("GET");
assertEquals(HTTP_BAD_METHOD, conn.getResponseCode());
assertEquals("POST", conn.getHeaderField("Allow"));
}
@Test
public void shouldOnlySupportJsonRequests() throws IOException {
startServer();
final HttpURLConnection conn = startRequest();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "text/plain; charset=utf-8");
conn.setDoOutput(true);
try (final OutputStream requestBody = conn.getOutputStream()) {
requestBody.write("This is not a JSON body.".getBytes(StandardCharsets.UTF_8));
}
assertEquals(HTTP_UNSUPPORTED_TYPE, conn.getResponseCode());
}
@Test
public void shouldRejectEmptyRequests() throws IOException {
startServer();
final HttpURLConnection conn = startJsonPostRequest();
assertEquals(HTTP_BAD_REQUEST, conn.getResponseCode());
}
@Test
public void shouldRejectRequestsWithoutPartyId() throws IOException {
startServer();
urlTemplate = Optional.of(JSON_EVENT_WITHOUT_PARTYID_URL_TEMPLATE);
final HttpURLConnection conn = request();
assertEquals(HTTP_BAD_REQUEST, conn.getResponseCode());
}
@Test
public void shouldRejectRequestsWithBrokenPartyId() throws IOException {
startServer();
urlTemplate = Optional.of(JSON_EVENT_WITH_BROKEN_PARTYID_URL_TEMPLATE);
final HttpURLConnection conn = request();
assertEquals(HTTP_BAD_REQUEST, conn.getResponseCode());
}
@Test
public void shouldAcceptRequestsWithoutContentLength() throws IOException {
startServer();
final HttpURLConnection conn = request(c -> {
// Chunked-streaming mode disables buffering and the content-length header.
c.setChunkedStreamingMode(1);
});
assertEquals(HTTP_NO_CONTENT, conn.getResponseCode());
}
@Test
public void shouldRejectRequestsWithBodyLessThanContentLength() throws IOException {
startServer();
final HttpURLConnection conn = startJsonPostRequest();
final byte[] bodyBytes = JSON_MAPPER.writeValueAsBytes(EMPTY_JSON_OBJECT);
// Explicitly lie about the length of the content we're sending.
conn.setChunkedStreamingMode(1);
conn.setRequestProperty("content-length", String.valueOf(bodyBytes.length + 1));
// Sanity check that the underlying implementation didn't discard our content length.
Assume.assumeTrue("Cannot manipulate content-length headers; try setting 'sun.net.http.allowRestrictedHeaders'.",
null != conn.getRequestProperty("content-length"));
try (final OutputStream requestBody = conn.getOutputStream()) {
requestBody.write(bodyBytes);
}
assertEquals(HTTP_BAD_REQUEST, conn.getResponseCode());
}
@Test
public void shouldRejectRequestsWithBodyMoreThanContentLength() throws IOException {
startServer();
final HttpURLConnection conn = startJsonPostRequest();
final byte[] bodyBytes = JSON_MAPPER.writeValueAsBytes(EMPTY_JSON_OBJECT);
// Explicitly lie about the length of the content we're sending.
conn.setChunkedStreamingMode(1);
conn.setRequestProperty("content-length", String.valueOf(bodyBytes.length - 1));
// Sanity check that the underlying implementation didn't discard our content length.
Assume.assumeTrue("Cannot manipulate content-length headers; try setting 'sun.net.http.allowRestrictedHeaders'.",
null != conn.getRequestProperty("content-length"));
try (final OutputStream requestBody = conn.getOutputStream()) {
requestBody.write(bodyBytes);
}
assertEquals(HTTP_BAD_REQUEST, conn.getResponseCode());
}
private static ObjectNode buildBigJsonPayload(final int payloadSize) {
final ObjectNode root = JSON_MAPPER.createObjectNode();
// {"p":"XXXXX"}
final int jsonOverhead = 8;
final char[] propertyValue = new char[payloadSize - jsonOverhead];
Arrays.fill(propertyValue, 'X');
root.put("p", new String(propertyValue));
return root;
}
@Test
public void shouldRejectRequestsWithTooLargeContentLength() throws IOException {
startServer();
final HttpURLConnection conn = startJsonPostRequest();
final byte[] bodyBytes = JSON_MAPPER.writeValueAsBytes(buildBigJsonPayload(JSON_MAXIMUM_BODY_SIZE + 1));
// Here we're explicitly declaring that we're going to be large.
conn.setFixedLengthStreamingMode(bodyBytes.length);
try (final OutputStream requestBody = conn.getOutputStream()) {
requestBody.write(bodyBytes);
}
assertEquals(HTTP_ENTITY_TOO_LARGE, conn.getResponseCode());
}
@Test
public void shouldRejectRequestsWithTooLargeBody() throws IOException {
startServer();
final HttpURLConnection conn = startJsonPostRequest();
// Here we're not declaring ahead of time that we will be too large. But the body will be.
conn.setChunkedStreamingMode(128);
try (final OutputStream requestBody = conn.getOutputStream()) {
JSON_MAPPER.writeValue(requestBody, buildBigJsonPayload(JSON_MAXIMUM_BODY_SIZE + 1));
}
assertEquals(HTTP_ENTITY_TOO_LARGE, conn.getResponseCode());
}
@Test
public void shouldAcceptRequestsWithMaximumAllowedBodySize() throws IOException {
startServer();
final HttpURLConnection conn = request(buildBigJsonPayload(JSON_MAXIMUM_BODY_SIZE));
assertEquals(HTTP_NO_CONTENT, conn.getResponseCode());
}
@Test
public void shouldAcceptBodyLargerThanOneChunk() throws IOException {
// Need to ensure the configuration allows a max body size greater than 2 chunks.
startServer(ImmutableMap.of("divolte.sources.test-json-source.maximum_body_size",
ChunkyByteBuffer.CHUNK_SIZE * 3));
// (Disable the content-length header to force chunked reading.)
final HttpURLConnection conn = request(buildBigJsonPayload(ChunkyByteBuffer.CHUNK_SIZE * 2),
c -> c.setChunkedStreamingMode(8));
assertEquals(HTTP_NO_CONTENT, conn.getResponseCode());
}
private static ObjectNode buildCompleteJsonEvent() {
final ObjectNode root = JSON_MAPPER.createObjectNode()
.put("event_type", "complete-event")
.put("client_timestamp_iso", "2016-06-01T18:54:16.123Z")
.put("session_id", "0:ikfm9ufy:4PxwWyVYSjqHW_ZZBQjZ3EiIovxYMBFY")
.put("event_id", "3c54b1491693aa646914e6feeb4a47d1")
.put("is_new_party", true)
.put("is_new_session", true);
root.putObject("parameters")
.put("parameter1", "value1");
return root;
}
@Test
public void shouldReceiveCompleteJsonEvent() throws IOException, InterruptedException {
startServer();
request(buildCompleteJsonEvent());
final ServerTestUtils.EventPayload eventPayload =
testServer.orElseThrow(IllegalStateException::new).waitForEvent();
final DivolteEvent receivedEvent = eventPayload.event;
assertFalse(receivedEvent.corruptEvent);
assertEquals("0:i1t84hgy:5AF359Zjq5kUy98u4wQjlIZzWGhN~GlG", receivedEvent.partyId.value);
assertEquals("0:ikfm9ufy:4PxwWyVYSjqHW_ZZBQjZ3EiIovxYMBFY", receivedEvent.sessionId.value);
assertEquals("3c54b1491693aa646914e6feeb4a47d1", receivedEvent.eventId);
assertEquals("json", receivedEvent.eventSource);
assertEquals(Optional.of("complete-event"), receivedEvent.eventType);
assertTrue(receivedEvent.newPartyId);
assertTrue(receivedEvent.firstInSession);
assertTrue(receivedEvent.requestStartTime.isAfter(Instant.now().minusSeconds(60)));
assertEquals(Instant.parse("2016-06-01T18:54:16.123Z"), receivedEvent.clientTime);
assertFalse(receivedEvent.browserEventData.isPresent());
final DivolteEvent.JsonEventData jsonEventData = receivedEvent.jsonEventData.orElseThrow(AssertionError::new);
assertNotNull(jsonEventData);
final JsonNode parameters = receivedEvent.eventParametersProducer.get().orElseThrow(AssertionError::new);
assertEquals("value1", parameters.path("parameter1").textValue());
}
private static ObjectNode buildMinimumJsonEvent() {
return JSON_MAPPER.createObjectNode()
.put("client_timestamp_iso", "2016-06-01T18:54:16.123Z")
.put("session_id", "0:ikfm9ufy:4PxwWyVYSjqHW_ZZBQjZ3EiIovxYMBFZ")
.put("event_id", "3c54b1491693aa646914e6feeb4a47d2")
.put("is_new_party", true)
.put("is_new_session", true);
}
@Test
public void shouldReceiveMinimumJsonEvent() throws IOException, InterruptedException {
startServer();
request(buildMinimumJsonEvent());
final ServerTestUtils.EventPayload eventPayload =
testServer.orElseThrow(IllegalStateException::new).waitForEvent();
final DivolteEvent receivedEvent = eventPayload.event;
assertFalse(receivedEvent.corruptEvent);
assertEquals("0:i1t84hgy:5AF359Zjq5kUy98u4wQjlIZzWGhN~GlG", receivedEvent.partyId.value);
assertEquals("0:ikfm9ufy:4PxwWyVYSjqHW_ZZBQjZ3EiIovxYMBFZ", receivedEvent.sessionId.value);
assertEquals("3c54b1491693aa646914e6feeb4a47d2", receivedEvent.eventId);
assertEquals("json", receivedEvent.eventSource);
assertFalse(receivedEvent.eventType.isPresent());
assertTrue(receivedEvent.newPartyId);
assertTrue(receivedEvent.firstInSession);
assertTrue(receivedEvent.requestStartTime.isAfter(Instant.now().minusSeconds(60)));
assertEquals(Instant.parse("2016-06-01T18:54:16.123Z"), receivedEvent.clientTime);
assertFalse(receivedEvent.browserEventData.isPresent());
final DivolteEvent.JsonEventData jsonEventData = receivedEvent.jsonEventData.orElseThrow(AssertionError::new);
assertNotNull(jsonEventData);
final Optional<JsonNode> eventParameters = receivedEvent.eventParametersProducer.get();
assertFalse(eventParameters.isPresent());
}
}