/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.brooklyn.feed.http; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import java.net.URL; import java.util.List; import java.util.concurrent.Callable; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityFunctions; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.entity.EntityInternal.FeedSupport; import org.apache.brooklyn.core.feed.FeedConfig; import org.apache.brooklyn.core.feed.PollConfig; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.http.BetterMockWebServer; import org.apache.brooklyn.util.http.HttpToolResponse; import org.apache.brooklyn.util.guava.Functionals; import org.apache.brooklyn.util.net.Networking; import org.apache.brooklyn.util.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.SocketPolicy; public class HttpFeedTest extends BrooklynAppUnitTestSupport { private static final Logger log = LoggerFactory.getLogger(HttpFeedTest.class); final static AttributeSensor<String> SENSOR_STRING = Sensors.newStringSensor("aString", ""); final static AttributeSensor<Integer> SENSOR_INT = Sensors.newIntegerSensor( "aLong", ""); private static final long TIMEOUT_MS = 10*1000; private BetterMockWebServer server; private URL baseUrl; private Location loc; private EntityLocal entity; private HttpFeed feed; @BeforeMethod(alwaysRun=true) @Override public void setUp() throws Exception { super.setUp(); server = BetterMockWebServer.newInstanceLocalhost(); for (int i = 0; i < 100; i++) { server.enqueue(new MockResponse().setResponseCode(200).addHeader("content-type: application/json").setBody("{\"foo\":\"myfoo\"}")); } server.play(); baseUrl = server.getUrl("/"); loc = app.newLocalhostProvisioningLocation(); entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)); app.start(ImmutableList.of(loc)); } @AfterMethod(alwaysRun=true) @Override public void tearDown() throws Exception { if (feed != null) feed.stop(); if (server != null) server.shutdown(); feed = null; super.tearDown(); } @Test public void testPollsAndParsesHttpGetResponse() throws Exception { feed = HttpFeed.builder() .entity(entity) .baseUrl(baseUrl) .poll(HttpPollConfig.forSensor(SENSOR_INT) .period(100) .onSuccess(HttpValueFunctions.responseCode())) .poll(HttpPollConfig.forSensor(SENSOR_STRING) .period(100) .onSuccess(HttpValueFunctions.stringContentsFunction())) .build(); assertSensorEventually(SENSOR_INT, (Integer)200, TIMEOUT_MS); assertSensorEventually(SENSOR_STRING, "{\"foo\":\"myfoo\"}", TIMEOUT_MS); } @Test public void testFeedDeDupe() throws Exception { testPollsAndParsesHttpGetResponse(); entity.addFeed(feed); log.info("Feed 0 is: "+feed); testPollsAndParsesHttpGetResponse(); log.info("Feed 1 is: "+feed); entity.addFeed(feed); FeedSupport feeds = ((EntityInternal)entity).feeds(); Assert.assertEquals(feeds.getFeeds().size(), 1, "Wrong feed count: "+feeds.getFeeds()); } @Test public void testSetsConnectionTimeout() throws Exception { feed = HttpFeed.builder() .entity(entity) .baseUrl(baseUrl) .poll(new HttpPollConfig<Integer>(SENSOR_INT) .period(100) .connectionTimeout(Duration.TEN_SECONDS) .socketTimeout(Duration.TEN_SECONDS) .onSuccess(HttpValueFunctions.responseCode())) .build(); assertSensorEventually(SENSOR_INT, (Integer)200, TIMEOUT_MS); } // TODO How to cause the other end to just freeze (similar to aws-ec2 when securityGroup port is not open)? @Test public void testSetsConnectionTimeoutWhenServerDisconnects() throws Exception { if (server != null) server.shutdown(); server = BetterMockWebServer.newInstanceLocalhost(); for (int i = 0; i < 100; i++) { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); } server.play(); baseUrl = server.getUrl("/"); feed = HttpFeed.builder() .entity(entity) .baseUrl(baseUrl) .poll(new HttpPollConfig<Integer>(SENSOR_INT) .period(100) .connectionTimeout(Duration.TEN_SECONDS) .socketTimeout(Duration.TEN_SECONDS) .onSuccess(HttpValueFunctions.responseCode()) .onException(Functions.constant(-1))) .build(); assertSensorEventually(SENSOR_INT, -1, TIMEOUT_MS); } @Test public void testPollsAndParsesHttpPostResponse() throws Exception { feed = HttpFeed.builder() .entity(entity) .baseUrl(baseUrl) .poll(new HttpPollConfig<Integer>(SENSOR_INT) .method("post") .period(100) .onSuccess(HttpValueFunctions.responseCode())) .poll(new HttpPollConfig<String>(SENSOR_STRING) .method("post") .period(100) .onSuccess(HttpValueFunctions.stringContentsFunction())) .build(); assertSensorEventually(SENSOR_INT, (Integer)200, TIMEOUT_MS); assertSensorEventually(SENSOR_STRING, "{\"foo\":\"myfoo\"}", TIMEOUT_MS); } @Test public void testUsesFailureHandlerOn4xx() throws Exception { server = BetterMockWebServer.newInstanceLocalhost(); for (int i = 0; i < 100; i++) { server.enqueue(new MockResponse() .setResponseCode(401) .setBody("Unauthorised")); } server.play(); feed = HttpFeed.builder() .entity(entity) .baseUrl(server.getUrl("/")) .poll(new HttpPollConfig<Integer>(SENSOR_INT) .period(100) .onSuccess(HttpValueFunctions.responseCode()) .onFailure(HttpValueFunctions.responseCode())) .poll(new HttpPollConfig<String>(SENSOR_STRING) .period(100) .onSuccess(HttpValueFunctions.stringContentsFunction()) .onFailure(Functions.constant("Failed"))) .build(); assertSensorEventually(SENSOR_INT, 401, TIMEOUT_MS); assertSensorEventually(SENSOR_STRING, "Failed", TIMEOUT_MS); server.shutdown(); } @Test public void testUsesExceptionHandlerOn4xxAndNoFailureHandler() throws Exception { server = BetterMockWebServer.newInstanceLocalhost(); for (int i = 0; i < 100; i++) { server.enqueue(new MockResponse() .setResponseCode(401) .setBody("Unauthorised")); } server.play(); feed = HttpFeed.builder() .entity(entity) .baseUrl(server.getUrl("/")) .poll(new HttpPollConfig<Integer>(SENSOR_INT) .period(100) .onSuccess(HttpValueFunctions.responseCode()) .onException(Functions.constant(-1))) .build(); assertSensorEventually(SENSOR_INT, -1, TIMEOUT_MS); server.shutdown(); } @Test(groups="Integration") // marked integration as it takes a wee while public void testSuspendResume() throws Exception { feed = HttpFeed.builder() .entity(entity) .baseUrl(baseUrl) .poll(new HttpPollConfig<Integer>(SENSOR_INT) .period(100) .onSuccess(HttpValueFunctions.responseCode())) .poll(new HttpPollConfig<String>(SENSOR_STRING) .period(100) .onSuccess(HttpValueFunctions.stringContentsFunction())) .build(); assertSensorEventually(SENSOR_INT, (Integer)200, TIMEOUT_MS); feed.suspend(); final int countWhenSuspended = server.getRequestCount(); Thread.sleep(500); if (server.getRequestCount() > countWhenSuspended+1) Assert.fail("Request count continued to increment while feed was suspended, from "+countWhenSuspended+" to "+server.getRequestCount()); feed.resume(); Asserts.succeedsEventually(new Runnable() { public void run() { assertTrue(server.getRequestCount() > countWhenSuspended + 1, "Request count failed to increment when feed was resumed, from " + countWhenSuspended + ", still at " + server.getRequestCount()); } }); } @Test(groups="Integration") // marked integration as it takes a wee while public void testStartSuspended() throws Exception { feed = HttpFeed.builder() .entity(entity) .baseUrl(baseUrl) .poll(HttpPollConfig.forSensor(SENSOR_INT) .period(100) .onSuccess(HttpValueFunctions.responseCode())) .poll(HttpPollConfig.forSensor(SENSOR_STRING) .period(100) .onSuccess(HttpValueFunctions.stringContentsFunction())) .suspended() .build(); Asserts.continually(MutableMap.of("timeout", 500), Entities.attributeSupplier(entity, SENSOR_INT), Predicates.<Integer>equalTo(null)); int countWhenSuspended = server.getRequestCount(); feed.resume(); Asserts.eventually(Entities.attributeSupplier(entity, SENSOR_INT), Predicates.<Integer>equalTo(200)); if (server.getRequestCount() <= countWhenSuspended) Assert.fail("Request count failed to increment when feed was resumed, from "+countWhenSuspended+", still at "+server.getRequestCount()); log.info("RUN: "+countWhenSuspended+" - "+server.getRequestCount()); } @Test public void testPollsAndParsesHttpErrorResponseLocal() throws Exception { int unboundPort = Networking.nextAvailablePort(10000); feed = HttpFeed.builder() .entity(entity) .baseUri("http://localhost:" + unboundPort + "/path/should/not/exist") .poll(new HttpPollConfig<String>(SENSOR_STRING) .onSuccess(Functions.constant("success")) .onFailure(Functions.constant("failure")) .onException(Functions.constant("error"))) .build(); assertSensorEventually(SENSOR_STRING, "error", TIMEOUT_MS); } @Test public void testPollsMulti() throws Exception { newMultiFeed(baseUrl); assertSensorEventually(SENSOR_INT, (Integer)200, TIMEOUT_MS); assertSensorEventually(SENSOR_STRING, "{\"foo\":\"myfoo\"}", TIMEOUT_MS); } // because takes a wee while @SuppressWarnings("rawtypes") @Test(groups="Integration") public void testPollsMultiClearsOnSubsequentFailure() throws Exception { server = BetterMockWebServer.newInstanceLocalhost(); for (int i = 0; i < 10; i++) { server.enqueue(new MockResponse() .setResponseCode(200) .setBody("Hello World")); } for (int i = 0; i < 10; i++) { server.enqueue(new MockResponse() .setResponseCode(401) .setBody("Unauthorised")); } server.play(); newMultiFeed(server.getUrl("/")); assertSensorEventually(SENSOR_INT, 200, TIMEOUT_MS); assertSensorEventually(SENSOR_STRING, "Hello World", TIMEOUT_MS); assertSensorEventually(SENSOR_INT, -1, TIMEOUT_MS); assertSensorEventually(SENSOR_STRING, null, TIMEOUT_MS); List<String> attrs = Lists.transform(MutableList.copyOf( ((EntityInternal)entity).getAllAttributes().keySet() ), new Function<AttributeSensor,String>() { @Override public String apply(AttributeSensor input) { return input.getName(); } }); Assert.assertTrue(!attrs.contains(SENSOR_STRING.getName()), "attrs contained "+SENSOR_STRING); Assert.assertTrue(!attrs.contains(FeedConfig.NO_SENSOR.getName()), "attrs contained "+FeedConfig.NO_SENSOR); server.shutdown(); } private void newMultiFeed(URL baseUrl) { feed = HttpFeed.builder() .entity(entity) .baseUrl(baseUrl) .poll(HttpPollConfig.forMultiple() .onSuccess(new Function<HttpToolResponse,Void>() { public Void apply(HttpToolResponse response) { entity.sensors().set(SENSOR_INT, response.getResponseCode()); if (response.getResponseCode()==200) entity.sensors().set(SENSOR_STRING, response.getContentAsString()); return null; } }) .onFailureOrException(Functionals.function(EntityFunctions.settingSensorsConstant(entity, MutableMap.<AttributeSensor<?>,Object>of( SENSOR_INT, -1, SENSOR_STRING, PollConfig.REMOVE)))) .period(100)) .build(); } private <T> void assertSensorEventually(final AttributeSensor<T> sensor, final T expectedVal, long timeout) { Asserts.succeedsEventually(ImmutableMap.of("timeout", timeout), new Callable<Void>() { public Void call() { assertEquals(entity.getAttribute(sensor), expectedVal); return null; } }); } }