com.spotify.apollo.elide.ElideResourceTest.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.apollo.elide.ElideResourceTest.java

Source

/*
 * -\-\-
 * Apollo Elide integration
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * 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 com.spotify.apollo.elide;

import static com.spotify.apollo.Status.CREATED;
import static com.spotify.apollo.Status.NOT_ACCEPTABLE;
import static com.spotify.apollo.Status.NO_CONTENT;
import static com.spotify.apollo.Status.OK;
import static com.spotify.apollo.Status.UNSUPPORTED_MEDIA_TYPE;
import static com.spotify.apollo.test.unit.ResponseMatchers.hasHeader;
import static com.spotify.apollo.test.unit.ResponseMatchers.hasStatus;
import static com.spotify.apollo.test.unit.StatusTypeMatchers.withCode;
import static java.util.Optional.empty;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.spotify.apollo.Client;
import com.spotify.apollo.Request;
import com.spotify.apollo.Response;
import com.spotify.apollo.Status;
import com.spotify.apollo.elide.ElideResource.Method;
import com.spotify.apollo.elide.testmodel.Thing;
import com.spotify.apollo.request.RequestContexts;
import com.spotify.apollo.request.RequestMetadataImpl;
import com.spotify.apollo.route.AsyncHandler;
import com.spotify.apollo.route.Route;
import com.spotify.apollo.test.StubClient;
import com.yahoo.elide.Elide;
import com.yahoo.elide.core.DataStoreTransaction;
import com.yahoo.elide.datastores.inmemory.InMemoryDataStore;
import java.io.IOException;
import java.time.Instant;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MultivaluedMap;
import okio.ByteString;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class ElideResourceTest {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final String PREFIX = "/prefix";

    private ElideResource resource;

    private Elide elide;
    private InMemoryDataStore dataStore;

    private Client client = new StubClient();

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Before
    public void setUp() throws Exception {
        dataStore = new InMemoryDataStore(Package.getPackage("com.spotify.apollo.elide.testmodel"));
        elide = new Elide.Builder(dataStore).build();

        resource = ElideResource.builder(PREFIX, elide).build();

        addToDataStore(new Thing("1", "flerp"));
        addToDataStore(new Thing("2", "florpe"));
    }

    @Test
    public void shouldReturnSingleElementFromElide() throws Exception {
        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing/1", "GET"));

        JsonNode jsonNode = successfulAsJson(response);

        assertThat(jsonNode.get("data").get("id").asText(), is("1"));
        assertThat(jsonNode.get("data").get("attributes").get("name").asText(), is("flerp"));
    }

    @Test
    public void shouldReturnCollectionFromElide() throws Exception {

        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing", "GET"));

        JsonNode jsonNode = successfulAsJson(response);

        Set<String> ids = new HashSet<>();
        Set<String> names = new HashSet<>();

        JsonNode data = jsonNode.get("data");

        for (int i = 0; i < data.size(); i++) {
            ids.add(data.get(i).get("id").asText());
            names.add(data.get(i).get("attributes").get("name").asText());
        }

        assertThat(ids, is(ImmutableSet.of("1", "2")));
        assertThat(names, is(ImmutableSet.of("flerp", "florpe")));
    }

    @Test
    public void shouldReturn404ForUnknownCollection() throws Exception {
        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/nonexistent", "GET"));

        assertThat(response, hasStatus(withCode(Status.NOT_FOUND)));
    }

    @Test
    public void shouldSupportSparseFieldsInGet() throws Exception {
        JsonNode jsonNode = successfulAsJson(
                invokeRoute(Request.forUri("/prefix/thing/1?fields[thing]=description", "GET")));

        assertThat(jsonNode.get("data").get("attributes").has("description"), is(true));
        assertThat(jsonNode.get("data").get("attributes").has("name"), is(false));
    }

    @Test
    public void shouldHaveNoRouteForDisabledMethod() throws Exception {
        EnumSet<Method> allButGet = EnumSet.complementOf(EnumSet.of(Method.GET));
        resource = ElideResource.builder(PREFIX, elide).enabledMethods(allButGet).build();

        assertThat(resource.routes().filter(r -> r.method().equals("GET")).findAny(), is(empty()));
    }

    @Test
    public void shouldSetContentHeader() throws Exception {
        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing", "GET"));

        assertThat(response, hasHeader("content-type", equalTo("application/vnd.api+json")));
    }

    @Test
    public void shouldSupportPrefixWithTrailingSlash() throws Exception {
        resource = ElideResource.builder(PREFIX + "/", elide).build();

        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing", "GET"));

        assertThat(response, hasStatus(withCode(Status.OK)));
    }

    @Test
    public void shouldFailForPrefixWithoutLeadingSlash() throws Exception {
        thrown.expect(IllegalArgumentException.class);

        ElideResource.builder(PREFIX.substring(1), elide);
    }

    @Test
    public void shouldSupportPost() throws Exception {
        Response<ByteString> response = invokeRoute(
                Request.forUri("/prefix/thing", "POST").withPayload(toBody(new Thing("3", "posted"))));

        if (response.payload().isPresent()) {
            assertThat(response, hasStatus(withCode(CREATED)));
        } else {
            assertThat(response, hasStatus(withCode(NO_CONTENT)));
        }
    }

    @Test
    public void shouldReturn201ForCreateWithoutId() throws Exception {
        Thing thing = new Thing("1", "hasNoId");
        thing.id = null;

        Response<ByteString> response = invokeRoute(
                Request.forUri("/prefix/thing", "POST").withPayload(toBody(thing)));

        JsonNode jsonNode = bodyWithExpectedStatus(response, CREATED);

        assertThat(jsonNode.get("data").get("attributes").get("name").asText(), is("hasNoId"));
    }

    @Test
    public void shouldSupportDelete() throws Exception {
        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing/1", "DELETE"));

        assertThat(response, hasStatus(withCode(NO_CONTENT)));
    }

    @Test
    public void shouldSupportPatch() throws Exception {
        Thing thing = new Thing("1", null);
        thing.description = "cooldesc";

        Response<ByteString> response = invokeRoute(
                Request.forUri("/prefix/thing/1", "PATCH").withPayload(toBody(thing)));

        if (response.payload().isPresent()) {
            assertThat(response, hasStatus(withCode(OK)));
        } else {
            assertThat(response, hasStatus(withCode(NO_CONTENT)));
        }

        response = invokeRoute(Request.forUri("/prefix/thing/1", "GET"));

        JsonNode jsonNode = successfulAsJson(response);

        assertThat(jsonNode.get("data").get("attributes").get("description").asText(), is("cooldesc"));
        assertThat(jsonNode.get("data").get("attributes").get("name").asText(), is("flerp"));
    }

    @Test
    public void shouldReturn415ForMediaTypeParametersInRequestContentType() throws Exception {
        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing", "POST")
                .withHeader("content-type", "application/vnd.api+json; charset=utf-8")
                .withPayload(toBody(new Thing("19", "hi"))));

        assertThat(response, hasStatus(withCode(UNSUPPORTED_MEDIA_TYPE)));
    }

    @Test
    public void shouldReturn406ForMediaTypeParametersInAllAcceptOptions() throws Exception {
        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing", "GET")
                .withHeader("Accept",
                        "application/vnd.api+json;charset=utf-8, application/vnd.api+json;charset=us-ascii")
                .withPayload(toBody(new Thing("19", "hi"))));

        assertThat(response, hasStatus(withCode(NOT_ACCEPTABLE)));
    }

    @Test
    public void shouldSupportMediaTypeParametersInOneAcceptOptions() throws Exception {
        Response<ByteString> response = invokeRoute(Request.forUri("/prefix/thing", "GET")
                .withHeader("Accept", "application/vnd.api+json;charset=utf-8,application/vnd.api+json")
                .withPayload(toBody(new Thing("19", "hi"))));

        assertThat(response, hasStatus(withCode(OK)));
    }

    @Test
    public void shouldPassUserSuppliedByFunctionToElide() throws Exception {
        Elide spiedElide = spy(elide);

        resource = ElideResource.builder(PREFIX, spiedElide).userFunction(rc -> rc.request().uri() + "-soopasecret")
                .build();

        invokeRoute(Request.forUri("/prefix/thing"));

        verify(spiedElide).get(anyString(), any(MultivaluedMap.class), eq("/prefix/thing-soopasecret"));
    }

    private void addToDataStore(Thing thing) {
        DataStoreTransaction transaction = dataStore.beginTransaction();
        transaction.preCommit();
        transaction.save(thing);
        transaction.commit();
    }

    private Response<ByteString> invokeRoute(Request request) throws Exception {
        List<Route<AsyncHandler<Response<ByteString>>>> routes = resource.routes()
                .filter(r -> r.method().equals(request.method())).collect(toList());

        assertThat(routes.size(), is(1));

        return routes.get(0).handler().invoke(RequestContexts.create(request, client, pathArgs(request.uri()), 0,
                RequestMetadataImpl.create(Instant.now(), empty(), empty()))).toCompletableFuture().get();
    }

    private Map<String, String> pathArgs(String uri) {
        int queryStartIndex = uri.indexOf('?');

        String queryPath = queryStartIndex < 0 ? uri.substring(PREFIX.length() + 1)
                : uri.substring(PREFIX.length() + 1, queryStartIndex);
        return ImmutableMap.of("query-path", queryPath);
    }

    private JsonNode successfulAsJson(Response<ByteString> response) throws Exception {
        return bodyWithExpectedStatus(response, OK);
    }

    private JsonNode bodyWithExpectedStatus(Response<ByteString> response, Status status) throws IOException {
        assertThat(response, hasStatus(withCode(status)));
        assertThat(response.payload().isPresent(), is(true));

        //noinspection OptionalGetWithoutIsPresent - checked above
        return OBJECT_MAPPER.readTree(response.payload().get().utf8());
    }

    private ByteString toBody(Thing thing) {
        // verry sophisticate json mappings indeed
        if (thing.name != null) {
            return ByteString.encodeUtf8(String.format(
                    "{ \"data\": {" + "\"id\": \"%s\"," + "\"type\": \"thing\"," + "\"attributes\": {"
                            + "\"name\": \"%s\", " + "\"description\": \"%s\"" + "}" + "}" + "}",
                    thing.id, thing.name, thing.description));
        }

        return ByteString.encodeUtf8(String.format("{ \"data\": {" + "\"id\": \"%s\"," + "\"type\": \"thing\","
                + "\"attributes\": {" + "\"description\": \"%s\"" + "}" + "}" + "}", thing.id, thing.description));
    }
}