/* * Copyright (c) 2011-2015 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.google.cloud.pubsub.client; import com.google.api.client.auth.oauth2.Credential; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockWebServer; import com.squareup.okhttp.mockwebserver.RecordedRequest; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; import java.net.URL; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import okio.Buffer; import static com.google.common.net.HttpHeaders.ACCEPT_ENCODING; import static com.google.common.net.HttpHeaders.AUTHORIZATION; import static com.google.common.net.HttpHeaders.CONNECTION; import static com.google.common.net.HttpHeaders.CONTENT_ENCODING; import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; import static com.google.common.net.HttpHeaders.CONTENT_TYPE; import static com.google.common.net.HttpHeaders.USER_AGENT; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; @RunWith(MockitoJUnitRunner.class) public class PubsubTest { @Rule public ExpectedException exception = ExpectedException.none(); public static final String PROJECT = "test-project"; public static final String TOPIC_1 = "topic-1"; public static final String TOPIC_2 = "topic-2"; public static final String SUBSCRIPTION_1 = "subscription-1"; public static final String SUBSCRIPTION_2 = "subscription-2"; public static final String BASE_PATH = "/v1/"; private final Credential credential = new Credential.Builder(mock(Credential.AccessMethod.class)).build(); private static final String ACCESS_TOKEN = "token"; private final MockWebServer server = new MockWebServer(); private URL baseUrl; private Pubsub pubsub; @Before public void setUp() throws Exception { server.start(); baseUrl = server.getUrl(BASE_PATH); credential.setAccessToken(ACCESS_TOKEN); pubsub = Pubsub.builder() .uri(baseUrl.toURI()) .credential(credential) .build(); } @After public void tearDown() throws Exception { pubsub.close(); } @Test public void testClose() throws Exception { assertThat(pubsub.closeFuture().isDone(), is(false)); pubsub.close(); pubsub.closeFuture().get(10, SECONDS); } @Test public void testListTopics() throws Exception { final PubsubFuture<TopicList> future = pubsub.listTopics(PROJECT); final String expectedPath = BASE_PATH + "projects/" + PROJECT + "/topics"; assertThat(future.operation(), is("list topics")); assertThat(future.method(), is("GET")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("GET")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); final TopicList response = TopicList.of(Topic.of(PROJECT, TOPIC_1), Topic.of(PROJECT, TOPIC_2)); server.enqueue(new MockResponse().setBody(json(response))); final TopicList topicList = future.get(10, SECONDS); assertThat(topicList, is(response)); } @Test public void testListTopicsWithPageToken() throws Exception { final String pageToken = "foo"; final String nextPageToken = "foo"; final PubsubFuture<TopicList> future = pubsub.listTopics(PROJECT, pageToken); final String expectedPath = BASE_PATH + "projects/" + PROJECT + "/topics?pageToken=" + pageToken; assertThat(future.operation(), is("list topics")); assertThat(future.method(), is("GET")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("GET")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); final TopicList response = TopicList.builder() .nextPageToken(nextPageToken) .topics(Topic.of(PROJECT, TOPIC_1), Topic.of(PROJECT, TOPIC_2)) .build(); server.enqueue(new MockResponse().setBody(json(response))); final TopicList topicList = future.get(10, SECONDS); assertThat(topicList, is(response)); } @Test public void testGetTopic() throws Exception { final PubsubFuture<Topic> future = pubsub.getTopic(PROJECT, TOPIC_1); final String expectedPath = BASE_PATH + "projects/" + PROJECT + "/topics/" + TOPIC_1; assertThat(future.operation(), is("get topic")); assertThat(future.method(), is("GET")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("GET")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); final Topic response = Topic.of(PROJECT, TOPIC_1); server.enqueue(new MockResponse().setBody(json(response))); final Topic topic = future.get(10, SECONDS); assertThat(topic, is(response)); } @Test public void testCreateTopic() throws Exception { final PubsubFuture<Topic> future = pubsub.createTopic(PROJECT, TOPIC_1); final String expectedPath = BASE_PATH + Topic.canonicalTopic(PROJECT, TOPIC_1); assertThat(future.operation(), is("create topic")); assertThat(future.method(), is("PUT")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(greaterThan(0L))); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("PUT")); assertThat(request.getPath(), is(expectedPath)); assertThat(request.getHeader(CONTENT_ENCODING), is("gzip")); assertThat(request.getHeader(CONTENT_LENGTH), is(String.valueOf(future.payloadSize()))); assertThat(request.getHeader(CONTENT_TYPE), is("application/json; charset=UTF-8")); assertRequestHeaders(request); final Topic response = Topic.of(PROJECT, TOPIC_1); server.enqueue(new MockResponse().setBody(json(response))); final Topic topic = future.get(10, SECONDS); assertThat(topic, is(response)); } @Test public void testDeleteTopic() throws Exception { final PubsubFuture<Void> future = pubsub.deleteTopic(PROJECT, TOPIC_1); final String expectedPath = BASE_PATH + Topic.canonicalTopic(PROJECT, TOPIC_1); assertThat(future.operation(), is("delete topic")); assertThat(future.method(), is("DELETE")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("DELETE")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); server.enqueue(new MockResponse()); future.get(10, SECONDS); } @Test public void testListSubscriptions() throws Exception { final PubsubFuture<SubscriptionList> future = pubsub.listSubscriptions(PROJECT); final String expectedPath = BASE_PATH + "projects/" + PROJECT + "/subscriptions"; assertThat(future.operation(), is("list subscriptions")); assertThat(future.method(), is("GET")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("GET")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); final SubscriptionList response = SubscriptionList.of(Subscription.of(PROJECT, SUBSCRIPTION_1, TOPIC_1), Subscription.of(PROJECT, SUBSCRIPTION_2, TOPIC_2)); server.enqueue(new MockResponse().setBody(json(response))); final SubscriptionList subscriptionList = future.get(10, SECONDS); assertThat(subscriptionList, is(response)); } @Test public void testListSubscriptionsWithPageToken() throws Exception { final String pageToken = "foo"; final String nextPageToken = "foo"; final PubsubFuture<SubscriptionList> future = pubsub.listSubscriptions(PROJECT, pageToken); final String expectedPath = BASE_PATH + "projects/" + PROJECT + "/subscriptions?pageToken=" + pageToken; assertThat(future.operation(), is("list subscriptions")); assertThat(future.method(), is("GET")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("GET")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); final SubscriptionList response = SubscriptionList.builder() .nextPageToken(nextPageToken) .subscriptions(Subscription.of(PROJECT, SUBSCRIPTION_1, TOPIC_1), Subscription.of(PROJECT, SUBSCRIPTION_2, TOPIC_2)) .build(); server.enqueue(new MockResponse().setBody(json(response))); final SubscriptionList subscriptionList = future.get(10, SECONDS); assertThat(subscriptionList, is(response)); } @Test public void testGetSubscription() throws Exception { final PubsubFuture<Subscription> future = pubsub.getSubscription(PROJECT, SUBSCRIPTION_1); final String expectedPath = BASE_PATH + "projects/" + PROJECT + "/subscriptions/" + SUBSCRIPTION_1; assertThat(future.operation(), is("get subscription")); assertThat(future.method(), is("GET")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("GET")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); final Subscription response = Subscription.of(PROJECT, SUBSCRIPTION_1, TOPIC_1); server.enqueue(new MockResponse().setBody(json(response))); final Subscription subscription = future.get(10, SECONDS); assertThat(subscription, is(response)); } @Test public void testCreateSubscription() throws Exception { final PubsubFuture<Subscription> future = pubsub.createSubscription(PROJECT, SUBSCRIPTION_1, TOPIC_1); final String expectedPath = BASE_PATH + Subscription.canonicalSubscription(PROJECT, SUBSCRIPTION_1); assertThat(future.operation(), is("create subscription")); assertThat(future.method(), is("PUT")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(greaterThan(0L))); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("PUT")); assertThat(request.getPath(), is(expectedPath)); assertThat(request.getHeader(CONTENT_ENCODING), is("gzip")); assertThat(request.getHeader(CONTENT_LENGTH), is(String.valueOf(future.payloadSize()))); assertThat(request.getHeader(CONTENT_TYPE), is("application/json; charset=UTF-8")); assertRequestHeaders(request); final Subscription response = Subscription.of(PROJECT, SUBSCRIPTION_1, TOPIC_1); server.enqueue(new MockResponse().setBody(json(response))); final Subscription subscription = future.get(10, SECONDS); assertThat(subscription, is(response)); } @Test public void testDeleteSubscription() throws Exception { final PubsubFuture<Void> future = pubsub.deleteSubscription(PROJECT, SUBSCRIPTION_1); final String expectedPath = BASE_PATH + Subscription.canonicalSubscription(PROJECT, SUBSCRIPTION_1); assertThat(future.operation(), is("delete subscription")); assertThat(future.method(), is("DELETE")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(0L)); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("DELETE")); assertThat(request.getPath(), is(expectedPath)); assertRequestHeaders(request); server.enqueue(new MockResponse()); future.get(10, SECONDS); } @Test public void testPublishSingle() throws Exception { final Message[] messages = {Message.of("m0")}; final String[] ids = {"id0"}; testPublish(messages, ids); } @Test public void testPublishNonBase64ShouldFail() throws Exception { final Message badMessage = Message.of("foo-bar"); exception.expect(IllegalArgumentException.class); pubsub.publish(PROJECT, TOPIC_1, badMessage); } @Test public void testPublishBatch() throws Exception { final Message[] messages = {Message.of("m0"), Message.of("m1"), Message.of("m2")}; final String[] ids = {"id0", "id1", "id2"}; testPublish(messages, ids); } private void testPublish(final Message[] messages, final String[] ids) throws InterruptedException, ExecutionException, TimeoutException { final PubsubFuture<List<String>> future = pubsub.publish(PROJECT, TOPIC_1, messages); final String expectedPath = BASE_PATH + Topic.canonicalTopic(PROJECT, TOPIC_1) + ":publish"; assertThat(future.operation(), is("publish")); assertThat(future.method(), is("POST")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(greaterThan(0L))); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("POST")); assertThat(request.getPath(), is(expectedPath)); assertThat(request.getHeader(CONTENT_ENCODING), is("gzip")); assertThat(request.getHeader(CONTENT_LENGTH), is(String.valueOf(future.payloadSize()))); assertThat(request.getHeader(CONTENT_TYPE), is("application/json; charset=UTF-8")); assertRequestHeaders(request); final ImmutableMap<String, ImmutableList<String>> response = ImmutableMap.of( "messageIds", ImmutableList.copyOf(ids)); server.enqueue(new MockResponse().setBody(json(response))); final List<String> messageIds = future.get(10, SECONDS); assertThat(messageIds, contains(ids)); } @Test public void testPullEmpty() throws InterruptedException, ExecutionException, TimeoutException { testPull(); } @Test public void testPullSingle() throws InterruptedException, ExecutionException, TimeoutException { testPull(ReceivedMessage.ofEncoded("a0", "m0")); } @Test public void testPullBatch() throws InterruptedException, ExecutionException, TimeoutException { testPull(ReceivedMessage.ofEncoded("a0", "m0"), ReceivedMessage.ofEncoded("a1", "m1"), ReceivedMessage.ofEncoded("a2", "m2")); } private void testPull(final ReceivedMessage... messages) throws InterruptedException, ExecutionException, TimeoutException { final PubsubFuture<List<ReceivedMessage>> future = pubsub.pull(PROJECT, SUBSCRIPTION_1); final String expectedPath = BASE_PATH + Subscription.canonicalSubscription(PROJECT, SUBSCRIPTION_1) + ":pull"; assertThat(future.operation(), is("pull")); assertThat(future.method(), is("POST")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(greaterThan(0L))); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("POST")); assertThat(request.getPath(), is(expectedPath)); assertThat(request.getHeader(CONTENT_ENCODING), is("gzip")); assertThat(request.getHeader(CONTENT_LENGTH), is(String.valueOf(future.payloadSize()))); assertThat(request.getHeader(CONTENT_TYPE), is("application/json; charset=UTF-8")); assertThat(request.getHeader(ACCEPT_ENCODING), containsString("gzip")); assertRequestHeaders(request); final ImmutableMap<String, ImmutableList<ReceivedMessage>> response = ImmutableMap.of( "receivedMessages", ImmutableList.copyOf(messages)); server.enqueue(new MockResponse().setBody(json(response))); final List<ReceivedMessage> receivedMessages = future.get(10, SECONDS); if (messages.length == 0) { assertThat(receivedMessages, is(empty())); } else { assertThat(receivedMessages, contains(messages)); } } @Test public void testAcknowledgeSingle() throws InterruptedException, ExecutionException, TimeoutException { testAcknowledge("a0"); } @Test public void testAcknowledgeBatch() throws InterruptedException, ExecutionException, TimeoutException { testAcknowledge("a0", "a1", "a2"); } private void testAcknowledge(final String... ackIds) throws InterruptedException, ExecutionException, TimeoutException { final PubsubFuture<Void> future = pubsub.acknowledge(PROJECT, SUBSCRIPTION_1, ackIds); final String expectedPath = BASE_PATH + Subscription.canonicalSubscription(PROJECT, SUBSCRIPTION_1) + ":acknowledge"; assertThat(future.operation(), is("acknowledge")); assertThat(future.method(), is("POST")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(greaterThan(0L))); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("POST")); assertThat(request.getPath(), is(expectedPath)); assertThat(request.getHeader(CONTENT_ENCODING), is("gzip")); assertThat(request.getHeader(CONTENT_LENGTH), is(String.valueOf(future.payloadSize()))); assertThat(request.getHeader(CONTENT_TYPE), is("application/json; charset=UTF-8")); assertRequestHeaders(request); server.enqueue(new MockResponse()); future.get(10, SECONDS); } @Test public void testModifyAckDeadlineSingle() throws Exception { testModifyAckDeadline(17, "a0"); } @Test public void testModifyAckDeadlineBatch() throws Exception { testModifyAckDeadline(17, "a0", "a1", "a2"); } private void testModifyAckDeadline(final int ackDeadlineSeconds, final String... ackIds) throws Exception { final PubsubFuture<Void> future = pubsub.modifyAckDeadline(PROJECT, SUBSCRIPTION_1, ackDeadlineSeconds, ackIds); final String expectedPath = BASE_PATH + Subscription.canonicalSubscription(PROJECT, SUBSCRIPTION_1) + ":modifyAckDeadline"; assertThat(future.operation(), is("modify ack deadline")); assertThat(future.method(), is("POST")); assertThat(future.uri(), is(server.getUrl(expectedPath).toString())); assertThat(future.payloadSize(), is(greaterThan(0L))); final RecordedRequest request = server.takeRequest(10, SECONDS); assertThat(request.getMethod(), is("POST")); assertThat(request.getPath(), is(expectedPath)); assertThat(request.getHeader(CONTENT_ENCODING), is("gzip")); assertThat(request.getHeader(CONTENT_LENGTH), is(String.valueOf(future.payloadSize()))); assertThat(request.getHeader(CONTENT_TYPE), is("application/json; charset=UTF-8")); assertRequestHeaders(request); server.enqueue(new MockResponse()); future.get(10, SECONDS); } @Test() public void testRequestFailure() throws Exception { final PubsubFuture<TopicList> future = pubsub.listTopics(PROJECT); server.enqueue(new MockResponse().setStatus("HTTP/1.1 500 ONOES")); final RequestFailedException failure = future.handle((v, ex) -> (RequestFailedException) ex).get(); assertThat(failure, is(notNullValue())); assertThat(failure.statusCode(), is(500)); assertThat(failure.statusMessage(), is("ONOES")); } @Test public void testPublishTimeoutFailureRecovery() throws Exception { pubsub = Pubsub.builder() .uri(baseUrl.toURI()) .maxConnections(1) .credential(credential) .requestTimeout(10) .readTimeout(10) .build(); final Message m1 = Message.of("1"); final Message m2 = Message.of("2"); // Time out first request final PubsubFuture<List<String>> f1 = pubsub.publish(PROJECT, "t1", m1); final TimeoutException timeout = f1.handle((v, ex) -> (TimeoutException) ex).get(); assertThat(timeout, is(notNullValue())); server.enqueue(new MockResponse().setBody(buffer(Json.write(PublishResponse.of("id1"))))); // Verify that a subsequent request is successful server.enqueue(new MockResponse().setBody(buffer(Json.write(PublishResponse.of("id2"))))); final CompletableFuture<List<String>> f2 = pubsub.publish("test", "t2", m2); final List<String> ids2 = f2.get(); assertThat(ids2, contains("id2")); } private void assertRequestHeaders(final RecordedRequest request) { assertThat(request.getHeader(USER_AGENT), anyOf(is("Spotify Google-HTTP-Java-Client/1.21.0 (gzip)"), is("Spotify-Google-Pubsub-Java-Client/1.0.0 (gzip)"))); assertThat(request.getHeader(AUTHORIZATION), is("Bearer " + ACCESS_TOKEN)); assertThat(request.getHeader(ACCEPT_ENCODING), anyOf(is("gzip,deflate"), is("gzip"))); assertThat(request.getHeader(CONNECTION), is("keep-alive")); } private static Buffer json(final Object o) { return buffer(Json.write(o)); } private static Buffer buffer(final byte[] bytes) { return new Buffer().write(bytes); } }