diff --git a/pom.xml b/pom.xml index 994a57d7..8b56f29b 100644 --- a/pom.xml +++ b/pom.xml @@ -203,6 +203,12 @@ 1.7.0 test + + com.toomuchcoding.jsonassert + jsonassert + 0.4.1 + test + diff --git a/spring-cloud-zookeeper-discovery/pom.xml b/spring-cloud-zookeeper-discovery/pom.xml index ae27ad5c..f9246e72 100644 --- a/spring-cloud-zookeeper-discovery/pom.xml +++ b/spring-cloud-zookeeper-discovery/pom.xml @@ -169,6 +169,11 @@ awaitility test + + com.toomuchcoding.jsonassert + jsonassert + test + diff --git a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/PollingUtils.groovy b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/PollingUtils.groovy deleted file mode 100644 index 86269bd3..00000000 --- a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/PollingUtils.groovy +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2013-2015 the original author or authors. - * - * 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 org.springframework.cloud.zookeeper.discovery - -trait PollingUtils { - Closure willPass(Closure closure) { - return { - try { - closure() - } catch (Exception e) { - AssertionError assertionError = new AssertionError("Exception occurred while evaluating closure", e) - System.err.println(assertionError) - throw assertionError - } - } - } - -} diff --git a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryClientSpec.groovy b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryClientSpec.groovy deleted file mode 100644 index 91925063..00000000 --- a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryClientSpec.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package org.springframework.cloud.zookeeper.discovery - -import spock.lang.Specification - -/** - * @author Marcin Grzejszczak - */ -class ZookeeperDiscoveryClientSpec extends Specification { - - def "should return an empty list of services if service discovery is null"() { - given: - ZookeeperServiceDiscovery serviceDiscovery = Stub() - ZookeeperDiscoveryClient zookeeperDiscoveryClient = new ZookeeperDiscoveryClient(serviceDiscovery, null) - when: - List services = zookeeperDiscoveryClient.getServices() - then: - assert services.empty - } -} - diff --git a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryClientTests.java b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryClientTests.java new file mode 100644 index 00000000..0559f668 --- /dev/null +++ b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryClientTests.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.zookeeper.discovery; + +import java.util.List; + +import org.junit.Test; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.Mockito.mock; + +/** + * @author Marcin Grzejszczak + */ +public class ZookeeperDiscoveryClientTests { + + @Test public void should_return_an_empty_list_of_services_if_service_discovery_is_null() { + // given: + ZookeeperServiceDiscovery serviceDiscovery = mock(ZookeeperServiceDiscovery.class); + ZookeeperDiscoveryClient zookeeperDiscoveryClient = new ZookeeperDiscoveryClient(serviceDiscovery, null); + // when: + List services = zookeeperDiscoveryClient.getServices(); + // then: + then(services).isEmpty(); + } +} diff --git a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryHealthIndicatorWithNestedStructureISpec.groovy b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryHealthIndicatorWithNestedStructureISpec.groovy deleted file mode 100644 index 1f75b2c9..00000000 --- a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryHealthIndicatorWithNestedStructureISpec.groovy +++ /dev/null @@ -1,79 +0,0 @@ -package org.springframework.cloud.zookeeper.discovery - -import groovy.json.JsonSlurper -import org.apache.curator.framework.CuratorFramework -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.EnableAutoConfiguration -import org.springframework.boot.test.SpringApplicationContextLoader -import org.springframework.boot.test.WebIntegrationTest -import org.springframework.cloud.client.discovery.EnableDiscoveryClient -import org.springframework.cloud.client.loadbalancer.LoadBalanced -import org.springframework.cloud.zookeeper.discovery.test.CommonTestConfig -import org.springframework.cloud.zookeeper.discovery.test.TestRibbonClient -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Import -import org.springframework.context.annotation.Profile -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration -import org.springframework.web.client.RestTemplate -import spock.lang.Issue -import spock.lang.Specification - -import javax.annotation.PostConstruct -import javax.annotation.PreDestroy - -@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader) -@ActiveProfiles('nestedstructure') -@WebIntegrationTest(randomPort = true) -class ZookeeperDiscoveryHealthIndicatorWithNestedStructureISpec extends Specification implements PollingUtils { - - @Autowired TestRibbonClient testRibbonClient - - @Issue("#54 - ZookeeperDiscoveryHealthIndicator fails on nested structure") - def 'should return a response that app is in a healthy state when nested folders in zookeeper are present'() { - when: - String response = testRibbonClient.callService('me', 'health') - then: - twoServicesArePresentedInHealthEndpoint(response) - } - - private boolean twoServicesArePresentedInHealthEndpoint(String response) { - def services = new JsonSlurper().parseText(response).zookeeperDiscovery.services - assert ['me', '/a/b/c/d/anotherservice'].every { expectedServiceName -> - services.any { service -> - expectedServiceName == service.name - } - } - return true - } - - @Configuration - @EnableAutoConfiguration - @EnableDiscoveryClient - @Import(CommonTestConfig) - @Profile('nestedstructure') - static class Config { - - @Autowired CuratorFramework curatorFramework - CustomZookeeperServiceDiscovery customZookeeperServiceDiscovery - - @PostConstruct - void registerNestedDependency() { - customZookeeperServiceDiscovery = new CustomZookeeperServiceDiscovery("/a/b/c/d/anotherservice", - '/services', curatorFramework).build() - } - - @PreDestroy - void unregisterServiceDiscovery() { - customZookeeperServiceDiscovery?.close() - } - - @Bean - TestRibbonClient testRibbonClient(@LoadBalanced RestTemplate restTemplate, - @Value('${spring.application.name}') String springAppName) { - return new TestRibbonClient(restTemplate, springAppName) - } - } -} diff --git a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryHealthIndicatorWithNestedStructureTests.java b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryHealthIndicatorWithNestedStructureTests.java new file mode 100644 index 00000000..0a9fd2fc --- /dev/null +++ b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryHealthIndicatorWithNestedStructureTests.java @@ -0,0 +1,88 @@ +package org.springframework.cloud.zookeeper.discovery; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import com.toomuchcoding.jsonassert.JsonAssertion; + +import org.apache.curator.framework.CuratorFramework; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.zookeeper.discovery.test.CommonTestConfig; +import org.springframework.cloud.zookeeper.discovery.test.TestRibbonClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.RestTemplate; + +import groovy.json.JsonSlurper; +import spock.lang.Issue; + +import static com.toomuchcoding.jsonassert.JsonAssertion.*; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = ZookeeperDiscoveryHealthIndicatorWithNestedStructureTests.Config.class) +@ActiveProfiles("nestedstructure") +@WebIntegrationTest(randomPort = true) +public class ZookeeperDiscoveryHealthIndicatorWithNestedStructureTests { + + @Autowired TestRibbonClient testRibbonClient; + + // Issue: #54 - ZookeeperDiscoveryHealthIndicator fails on nested structure + @Test public void should_return_a_response_that_app_is_in_a_healthy_state_when_nested_folders_in_zookeeper_are_present() { + // when: + String response = this.testRibbonClient.callService("me", "health"); + // then: + twoServicesArePresentedInHealthEndpoint(response); + } + + private boolean twoServicesArePresentedInHealthEndpoint(String response) { + assertThat(response).field("zookeeperDiscovery").array("services").field("name").isEqualTo("me"); + assertThat(response).field("zookeeperDiscovery").array("services").field("name").isEqualTo("/a/b/c/d/anotherservice"); + return true; + } + + + @Configuration + @EnableAutoConfiguration + @EnableDiscoveryClient + @Import(CommonTestConfig.class) + @Profile("nestedstructure") + static class Config { + + @Autowired CuratorFramework curatorFramework; + CustomZookeeperServiceDiscovery customZookeeperServiceDiscovery; + + @PostConstruct + void registerNestedDependency() { + this.customZookeeperServiceDiscovery = new CustomZookeeperServiceDiscovery("/a/b/c/d/anotherservice", + "/services", this.curatorFramework); + this.customZookeeperServiceDiscovery.build(); + } + + @PreDestroy + void unregisterServiceDiscovery() { + if (this.customZookeeperServiceDiscovery != null) { + this.customZookeeperServiceDiscovery.close(); + } + } + + @Bean TestRibbonClient testRibbonClient(@LoadBalanced RestTemplate restTemplate, + @Value("${spring.application.name}") String springAppName) { + return new TestRibbonClient(restTemplate, springAppName); + } + } +} diff --git a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryISpec.groovy b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryISpec.groovy deleted file mode 100644 index 409eacd3..00000000 --- a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryISpec.groovy +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2013-2015 the original author or authors. - * - * 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 org.springframework.cloud.zookeeper.discovery - -import groovy.json.JsonSlurper -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.EnableAutoConfiguration -import org.springframework.boot.test.SpringApplicationContextLoader -import org.springframework.boot.test.WebIntegrationTest -import org.springframework.cloud.client.ServiceInstance -import org.springframework.cloud.client.discovery.DiscoveryClient -import org.springframework.cloud.client.discovery.EnableDiscoveryClient -import org.springframework.cloud.client.loadbalancer.LoadBalanced -import org.springframework.cloud.netflix.feign.EnableFeignClients -import org.springframework.cloud.netflix.feign.FeignClient -import org.springframework.cloud.zookeeper.discovery.test.CommonTestConfig -import org.springframework.cloud.zookeeper.discovery.test.TestRibbonClient -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Import -import org.springframework.context.annotation.Profile -import org.springframework.stereotype.Controller -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod -import org.springframework.web.client.RestTemplate -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -@ContextConfiguration(classes = Config, loader = SpringApplicationContextLoader) -@ActiveProfiles('ribbon') -@WebIntegrationTest(randomPort = true) -class ZookeeperDiscoveryISpec extends Specification implements PollingUtils { - - @Autowired TestRibbonClient testRibbonClient - @Autowired DiscoveryClient discoveryClient - @Autowired ZookeeperServiceDiscovery serviceDiscovery - @Value('${spring.application.name}') String springAppName - @Autowired IdUsingFeignClient idUsingFeignClient - PollingConditions conditions = new PollingConditions() - - - def 'should find the app by its name via Ribbon'() { - expect: - 'UP' == registeredServiceStatusViaServiceName() - } - - def 'should find a collaborator via discovery client'() { - given: - List instances = discoveryClient.getInstances(springAppName) - ServiceInstance instance = instances.first() - expect: - 'UP' == registeredServiceStatus(instance) - 'testMetadataValue' == instance.metadata.get('testMetadataKey') - } - - def 'should present application name as id of the service instance'() { - given: - ServiceInstance instance = discoveryClient.getLocalServiceInstance() - expect: - springAppName == instance.serviceId - } - - def 'should find an instance using feign via service id'() { - expect: - conditions.eventually willPass { - assert idUsingFeignClient.beans - } - } - - private String registeredServiceStatusViaServiceName() { - return new JsonSlurper().parseText(testRibbonClient.thisHealthCheck()).status - } - - private String registeredServiceStatus(ServiceInstance instance) { - return new JsonSlurper().parseText(testRibbonClient.callOnUrl("${instance.host}:${instance.port}", 'health')).status - } - - def 'should properly find local instance'() { - expect: - serviceDiscovery.serviceInstance.address == discoveryClient.localServiceInstance.host - } - - @FeignClient("ribbonApp") - public static interface IdUsingFeignClient { - @RequestMapping(method = RequestMethod.GET, value = "/beans") - String getBeans(); - } - - @Configuration - @EnableAutoConfiguration - @Import(CommonTestConfig) - @EnableDiscoveryClient - @EnableFeignClients(clients = [IdUsingFeignClient]) - @Profile('ribbon') - static class Config { - - @Bean - TestRibbonClient testRibbonClient(@LoadBalanced RestTemplate restTemplate, - @Value('${spring.application.name}') String springAppName) { - return new TestRibbonClient(restTemplate, springAppName) - } - } - - @Controller - @Profile('ribbon') - class PingController { - - @RequestMapping('/ping') String ping() { - return 'pong' - } - } -} diff --git a/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryTests.java b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryTests.java new file mode 100644 index 00000000..be273acf --- /dev/null +++ b/spring-cloud-zookeeper-discovery/src/test/groovy/org/springframework/cloud/zookeeper/discovery/ZookeeperDiscoveryTests.java @@ -0,0 +1,127 @@ +package org.springframework.cloud.zookeeper.discovery; + +import java.util.List; +import java.util.concurrent.Callable; + +import com.jayway.awaitility.Awaitility; +import com.toomuchcoding.jsonassert.JsonAssertion; +import com.toomuchcoding.jsonassert.JsonPath; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.netflix.feign.EnableFeignClients; +import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.cloud.zookeeper.discovery.test.CommonTestConfig; +import org.springframework.cloud.zookeeper.discovery.test.TestRibbonClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Marcin Grzejszczak + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(ZookeeperDiscoveryTests.Config.class) +@ActiveProfiles("ribbon") +@WebIntegrationTest(randomPort = true) +public class ZookeeperDiscoveryTests { + + @Autowired TestRibbonClient testRibbonClient; + @Autowired DiscoveryClient discoveryClient; + @Autowired ZookeeperServiceDiscovery serviceDiscovery; + @Value("${spring.application.name}") String springAppName; + @Autowired IdUsingFeignClient idUsingFeignClient; + + @Test public void should_find_the_app_by_its_name_via_Ribbon() { + //expect: + then(registeredServiceStatusViaServiceName()).isEqualTo("UP"); + } + + @Test public void should_find_a_collaborator_via_discovery_client() { + //given: + List instances = this.discoveryClient.getInstances(this.springAppName); + ServiceInstance instance = instances.get(0); + //expect: + then(registeredServiceStatus(instance)).isEqualTo("UP"); + then(instance.getMetadata().get("testMetadataKey")).isEqualTo("testMetadataValue"); + } + + @Test public void should_present_application_name_as_id_of_the_service_instance() { + //given: + ServiceInstance instance = this.discoveryClient.getLocalServiceInstance(); + //expect: + then(this.springAppName).isEqualTo(instance.getServiceId()); + } + + @Test public void should_find_an_instance_using_feign_via_service_id() { + final IdUsingFeignClient idUsingFeignClient = this.idUsingFeignClient; + //expect: + Awaitility.await().until(new Callable() { + @Override public Boolean call() throws Exception { + then(idUsingFeignClient.getBeans()).isNotEmpty(); + return true; + } + }); + } + + private String registeredServiceStatusViaServiceName() { + return JsonPath.builder(this.testRibbonClient.thisHealthCheck()).field("status").read(String.class); + } + + private String registeredServiceStatus(ServiceInstance instance) { + return JsonPath.builder(this.testRibbonClient.callOnUrl(instance.getHost()+":"+instance.getPort(), "health")).field("status").read(String.class); + } + + @Test public void should_properly_find_local_instance() { + //expect: + then(this.serviceDiscovery.getServiceInstance().getAddress()).isEqualTo(this.discoveryClient.getLocalServiceInstance().getHost()); + } + + + @FeignClient("ribbonApp") + public static interface IdUsingFeignClient { + @RequestMapping(method = RequestMethod.GET, value = "/beans") + String getBeans(); + } + + @Configuration + @EnableAutoConfiguration + @Import(CommonTestConfig.class) + @EnableDiscoveryClient + @EnableFeignClients(clients = { IdUsingFeignClient.class }) + @Profile("ribbon") + static class Config { + + @Bean TestRibbonClient testRibbonClient(@LoadBalanced RestTemplate restTemplate, + @Value("${spring.application.name}") String springAppName) { + return new TestRibbonClient(restTemplate, springAppName); + } + } + + @Controller + @Profile("ribbon") + class PingController { + + @RequestMapping("/ping") String ping() { + return "pong"; + } + } +}