/*
 * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.kubernetes;

import com.github.tomakehurst.wiremock.client.MappingBuilder;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.hazelcast.kubernetes.KubernetesClient.Endpoint;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;

public class KubernetesClientTest {
    private static final String KUBERNETES_MASTER_IP = "localhost";

    private static final String TOKEN = "sample-token";
    private static final String CA_CERTIFICATE = "sample-ca-certificate";
    private static final String NAMESPACE = "sample-namespace";
    private static final int RETRIES = 3;

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());

    private KubernetesClient kubernetesClient;

    @Before
    public void setUp() {
        kubernetesClient = newKubernetesClient(false);
        stubFor(get(urlMatching("/api/.*")).atPriority(5)
                                           .willReturn(aResponse().withStatus(401).withBody("\"reason\":\"Forbidden\"")));
    }

    @Test
    public void endpointsByNamespace() {
        // given
        //language=JSON
        String podsListResponse = "{\n"
                + "  \"items\": [\n"
                + "    {\n"
                + "      \"spec\": {\n"
                + "        \"containers\": [\n"
                + "          {\n"
                + "            \"ports\": [\n"
                + "              {\n"
                + "                \"containerPort\": 5701\n"
                + "              }\n"
                + "            ]\n"
                + "          }\n"
                + "        ]\n"
                + "      },\n"
                + "      \"status\": {\n"
                + "        \"podIP\": \"192.168.0.25\",\n"
                + "        \"containerStatuses\": [\n"
                + "          {\n"
                + "            \"ready\": true\n"
                + "          }\n"
                + "        ]\n"
                + "      }\n"
                + "    },\n"
                + "    {\n"
                + "      \"spec\": {\n"
                + "        \"containers\": [\n"
                + "          {\n"
                + "            \"ports\": [\n"
                + "              {\n"
                + "                \"containerPort\": 5702\n"
                + "              }\n"
                + "            ]\n"
                + "          }\n"
                + "        ]\n"
                + "      },\n"
                + "      \"status\": {\n"
                + "        \"podIP\": \"172.17.0.5\",\n"
                + "        \"containerStatuses\": [\n"
                + "          {\n"
                + "            \"ready\": true\n"
                + "          }\n"
                + "        ]\n"
                + "      }\n"
                + "    },\n"
                + "    {\n"
                + "      \"spec\": {\n"
                + "        \"containers\": [\n"
                + "          {\n"
                + "            \"ports\": [\n"
                + "              {\n"
                + "              }\n"
                + "            ]\n"
                + "          }\n"
                + "        ]\n"
                + "      },\n"
                + "      \"status\": {\n"
                + "        \"podIP\": \"172.17.0.6\",\n"
                + "        \"containerStatuses\": [\n"
                + "          {\n"
                + "            \"ready\": false\n"
                + "          }\n"
                + "        ]\n"
                + "      }\n"
                + "    }\n"
                + "  ]\n"
                + "}";
        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse);

        // when
        List<Endpoint> result = kubernetesClient.endpoints();

        // then
        assertThat(format(result),
                containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702), notReady("172.17.0.6", null)));
    }

    @Test
    public void endpointsByNamespaceAndServiceLabel() {
        // given
        //language=JSON
        String endpointsListResponse = "{\n"
                + "  \"kind\": \"EndpointsList\",\n"
                + "  \"items\": [\n"
                + "    {\n"
                + "      \"subsets\": [\n"
                + "        {\n"
                + "          \"addresses\": [\n"
                + "            {\n"
                + "              \"ip\": \"192.168.0.25\",\n"
                + "              \"hazelcast-service-port\": 5701\n"
                + "            }\n"
                + "          ]\n"
                + "        }\n"
                + "      ]\n"
                + "    },\n"
                + "    {\n"
                + "      \"subsets\": [\n"
                + "        {\n"
                + "          \"addresses\": [\n"
                + "            {\n"
                + "              \"ip\": \"172.17.0.5\",\n"
                + "              \"hazelcast-service-port\": 5702\n"
                + "            }\n"
                + "          ],\n"
                + "          \"notReadyAddresses\": [\n"
                + "            {\n"
                + "              \"ip\": \"172.17.0.6\"\n"
                + "            }\n"
                + "          ],\n"
                + "          \"ports\": [\n"
                + "            {\n"
                + "              \"port\": 5701\n"
                + "            }\n"
                + "          ]\n"
                + "        }\n"
                + "      ]\n"
                + "    }\n"
                + "  ]\n"
                + "}";
        String serviceLabel = "sample-service-label";
        String serviceLabelValue = "sample-service-label-value";
        Map<String, String> queryParams = singletonMap("labelSelector", String.format("%s=%s", serviceLabel, serviceLabelValue));
        stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), queryParams, endpointsListResponse);

        // when
        List<Endpoint> result = kubernetesClient.endpointsByServiceLabel(serviceLabel, serviceLabelValue);

        // then
        assertThat(format(result),
                containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702), notReady("172.17.0.6", 5701)));
    }

    @Test
    public void endpointsByNamespaceAndServiceName() {
        // given
        //language=JSON
        String endpointResponse = "{\n"
                + "  \"kind\": \"Endpoints\",\n"
                + "  \"subsets\": [\n"
                + "    {\n"
                + "      \"addresses\": [\n"
                + "        {\n"
                + "          \"ip\": \"192.168.0.25\",\n"
                + "          \"hazelcast-service-port\": 5701\n"
                + "        },\n"
                + "        {\n"
                + "          \"ip\": \"172.17.0.5\",\n"
                + "          \"hazelcast-service-port\": 5702\n"
                + "        }\n"
                + "      ]\n"
                + "    }\n"
                + "  ]\n"
                + "}";
        String serviceName = "service-name";
        stub(String.format("/api/v1/namespaces/%s/endpoints/%s", NAMESPACE, serviceName), endpointResponse);

        // when
        List<Endpoint> result = kubernetesClient.endpointsByName(serviceName);

        // then
        assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
    }

    @Test
    public void endpointsByNamespaceAndPodLabel() {
        // given
        //language=JSON
        String podsListResponse = "{\n"
                + "  \"kind\": \"PodList\",\n"
                + "  \"items\": [\n"
                + "    {\n"
                + "      \"spec\": {\n"
                + "        \"containers\": [\n"
                + "          {\n"
                + "            \"ports\": [\n"
                + "              {\n"
                + "                \"containerPort\": 5701\n"
                + "              }\n"
                + "            ]\n"
                + "          }\n"
                + "        ]\n"
                + "      },\n"
                + "      \"status\": {\n"
                + "        \"podIP\": \"192.168.0.25\",\n"
                + "        \"containerStatuses\": [\n"
                + "          {\n"
                + "            \"ready\": true\n"
                + "          }\n"
                + "        ]\n"
                + "      }\n"
                + "    },\n"
                + "    {\n"
                + "      \"spec\": {\n"
                + "        \"containers\": [\n"
                + "          {\n"
                + "            \"ports\": [\n"
                + "              {\n"
                + "                \"containerPort\": 5702\n"
                + "              }\n"
                + "            ]\n"
                + "          }\n"
                + "        ]\n"
                + "      },\n"
                + "      \"status\": {\n"
                + "        \"podIP\": \"172.17.0.5\",\n"
                + "        \"containerStatuses\": [\n"
                + "          {\n"
                + "            \"ready\": true\n"
                + "          }\n"
                + "        ]\n"
                + "      }\n"
                + "    }\n"
                + "  ]\n"
                + "}";

        String podLabel = "sample-pod-label";
        String podLabelValue = "sample-pod-label-value";
        Map<String, String> queryParams = singletonMap("labelSelector", String.format("%s=%s", podLabel, podLabelValue));
        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE, podLabel), queryParams, podsListResponse);

        // when
        List<Endpoint> result = kubernetesClient.endpointsByPodLabel(podLabel, podLabelValue);

        // then
        assertThat(format(result),
                containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
    }

    @Test
    public void zoneBeta() {
        // given
        String podName = "pod-name";

        //language=JSON
        String podResponse = "{\n"
                + "  \"kind\": \"Pod\",\n"
                + "  \"spec\": {\n"
                + "    \"nodeName\": \"node-name\"\n"
                + "  }\n"
                + "}";
        stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse);

        //language=JSON
        String nodeResponse = "{\n"
                + "  \"kind\": \"Node\",\n"
                + "  \"metadata\": {\n"
                + "    \"labels\": {\n"
                + "      \"failure-domain.beta.kubernetes.io/region\": \"us-central1\",\n"
                + "      \"failure-domain.beta.kubernetes.io/zone\": \"us-central1-a\"\n"
                + "    }\n"
                + "  }\n"
                + "}";
        stub("/api/v1/nodes/node-name", nodeResponse);

        // when
        String zone = kubernetesClient.zone(podName);

        // then
        assertEquals("us-central1-a", zone);
    }

    @Test
    public void zone() {
        // given
        String podName = "pod-name";

        //language=JSON
        String podResponse = "{\n"
                + "  \"kind\": \"Pod\",\n"
                + "  \"spec\": {\n"
                + "    \"nodeName\": \"node-name\"\n"
                + "  }\n"
                + "}";
        stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse);

        //language=JSON
        String nodeResponse = "{\n"
                + "  \"kind\": \"Node\",\n"
                + "  \"metadata\": {\n"
                + "    \"labels\": {\n"
                + "      \"failure-domain.beta.kubernetes.io/region\": \"deprecated-region\",\n"
                + "      \"failure-domain.beta.kubernetes.io/zone\": \"deprecated-zone\",\n"
                + "      \"failure-domain.kubernetes.io/region\": \"us-central1\",\n"
                + "      \"failure-domain.kubernetes.io/zone\": \"us-central1-a\"\n"
                + "    }\n"
                + "  }\n"
                + "}";
        stub("/api/v1/nodes/node-name", nodeResponse);

        // when
        String zone = kubernetesClient.zone(podName);

        // then
        assertEquals("us-central1-a", zone);
    }

    @Test
    public void endpointsByNamespaceWithLoadBalancerPublicIp() {
        // given
        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse());
        stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse());

        //language=JSON
        String serviceResponse1 = "{\n"
                + "  \"kind\": \"Service\",\n"
                + "  \"spec\": {\n"
                + "    \"ports\": [\n"
                + "      {\n"
                + "        \"port\": 32123,\n"
                + "        \"targetPort\": 5701,\n"
                + "        \"nodePort\": 31916\n"
                + "      }\n"
                + "    ]\n"
                + "  },\n"
                + "  \"status\": {\n"
                + "    \"loadBalancer\": {\n"
                + "      \"ingress\": [\n"
                + "        {\n"
                + "          \"ip\": \"35.232.226.200\"\n"
                + "        }\n"
                + "      ]\n"
                + "    }\n"
                + "  }\n"
                + "}\n";
        stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), serviceResponse1);

        //language=JSON
        String serviceResponse2 = "{\n"
                + "  \"kind\": \"Service\",\n"
                + "  \"spec\": {\n"
                + "    \"ports\": [\n"
                + "      {\n"
                + "        \"port\": 32124,\n"
                + "        \"targetPort\": 5701,\n"
                + "        \"nodePort\": 31916\n"
                + "      }\n"
                + "    ]\n"
                + "  },\n"
                + "  \"status\": {\n"
                + "    \"loadBalancer\": {\n"
                + "      \"ingress\": [\n"
                + "        {\n"
                + "          \"ip\": \"35.232.226.201\"\n"
                + "        }\n"
                + "      ]\n"
                + "    }\n"
                + "  }\n"
                + "}";
        stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), serviceResponse2);

        // when
        List<Endpoint> result = kubernetesClient.endpoints();

        // then
        assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
        assertThat(formatPublic(result), containsInAnyOrder(ready("35.232.226.200", 32123), ready("35.232.226.201", 32124)));
    }

    @Test
    public void endpointsByNamespaceWithNodePublicIp() {
        // given
        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse());
        stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse());

        stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), nodePortService1Response());
        stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), nodePortService2Response());

        //language=JSON
        String nodeResponse1 = "{\n"
                + "  \"kind\": \"Node\",\n"
                + "  \"status\": {\n"
                + "    \"addresses\": [\n"
                + "      {\n"
                + "        \"type\": \"InternalIP\",\n"
                + "        \"address\": \"10.240.0.21\"\n"
                + "      },\n"
                + "      {\n"
                + "        \"type\": \"ExternalIP\",\n"
                + "        \"address\": \"35.232.226.200\"\n"
                + "      }\n"
                + "    ]\n"
                + "  }\n"
                + "}\n";
        stub("/api/v1/nodes/node-name-1", nodeResponse1);

        String nodeResponse2 = "{\n"
                + "  \"kind\": \"Node\",\n"
                + "  \"status\": {\n"
                + "    \"addresses\": [\n"
                + "      {\n"
                + "        \"type\": \"InternalIP\",\n"
                + "        \"address\": \"10.240.0.22\"\n"
                + "      },\n"
                + "      {\n"
                + "        \"type\": \"ExternalIP\",\n"
                + "        \"address\": \"35.232.226.201\"\n"
                + "      }\n"
                + "    ]\n"
                + "  }\n"
                + "}\n";
        stub("/api/v1/nodes/node-name-2", nodeResponse2);

        // when
        List<Endpoint> result = kubernetesClient.endpoints();

        // then
        assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
        assertThat(formatPublic(result), containsInAnyOrder(ready("35.232.226.200", 31916), ready("35.232.226.201", 31917)));
    }

    @Test
    public void endpointsByNamespaceWithNodeName() {
        // given
        // create KubernetesClient with useNodeNameAsExternalAddress=true
        kubernetesClient = newKubernetesClient(true);

        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse());
        stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse());

        stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), nodePortService1Response());
        stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), nodePortService2Response());

        String forbiddenBody = "\"reason\":\"Forbidden\"";
        stub("/api/v1/nodes/node-name-1", 403, forbiddenBody);
        stub("/api/v1/nodes/node-name-2", 403, forbiddenBody);

        // when
        List<Endpoint> result = kubernetesClient.endpoints();

        // then
        assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
        assertThat(formatPublic(result), containsInAnyOrder(ready("node-name-1", 31916), ready("node-name-2", 31917)));
    }

    private static String podsListResponse() {
        //language=JSON
        return "{\n"
                + "  \"kind\": \"PodList\",\n"
                + "  \"items\": [\n"
                + "    {\n"
                + "      \"spec\": {\n"
                + "        \"containers\": [\n"
                + "          {\n"
                + "            \"ports\": [\n"
                + "              {\n"
                + "                \"containerPort\": 5701\n"
                + "              }\n"
                + "            ]\n"
                + "          }\n"
                + "        ]\n"
                + "      },\n"
                + "      \"status\": {\n"
                + "        \"podIP\": \"192.168.0.25\",\n"
                + "        \"containerStatuses\": [\n"
                + "          {\n"
                + "            \"ready\": true\n"
                + "          }\n"
                + "        ]\n"
                + "      }\n"
                + "    },\n"
                + "    {\n"
                + "      \"spec\": {\n"
                + "        \"containers\": [\n"
                + "          {\n"
                + "            \"ports\": [\n"
                + "              {\n"
                + "                \"containerPort\": 5702\n"
                + "              }\n"
                + "            ]\n"
                + "          }\n"
                + "        ]\n"
                + "      },\n"
                + "      \"status\": {\n"
                + "        \"podIP\": \"172.17.0.5\",\n"
                + "        \"containerStatuses\": [\n"
                + "          {\n"
                + "            \"ready\": true\n"
                + "          }\n"
                + "        ]\n"
                + "      }\n"
                + "    }\n"
                + "  ]\n"
                + "}";
    }

    private static String endpointsListResponse() {
        //language=JSON
        return "{\n"
                + "  \"kind\": \"EndpointsList\",\n"
                + "  \"items\": [\n"
                + "    {\n"
                + "      \"metadata\": {\n"
                + "        \"name\": \"my-release-hazelcast\"\n"
                + "      },\n"
                + "      \"subsets\": [\n"
                + "        {\n"
                + "          \"addresses\": [\n"
                + "            {\n"
                + "              \"ip\": \"192.168.0.25\",\n"
                + "              \"nodeName\": \"node-name-1\"\n"
                + "            },\n"
                + "            {\n"
                + "              \"ip\": \"172.17.0.5\",\n"
                + "              \"nodeName\": \"node-name-2\"\n"
                + "            }\n"
                + "          ],\n"
                + "          \"ports\": [\n"
                + "            {\n"
                + "              \"port\": 5701\n"
                + "            }\n"
                + "          ]\n"
                + "        }\n"
                + "      ]\n"
                + "    },\n"
                + "    {\n"
                + "      \"metadata\": {\n"
                + "        \"name\": \"service-0\"\n"
                + "      },\n"
                + "      \"subsets\": [\n"
                + "        {\n"
                + "          \"addresses\": [\n"
                + "            {\n"
                + "              \"ip\": \"192.168.0.25\",\n"
                + "              \"nodeName\": \"node-name-1\"\n"
                + "            }\n"
                + "          ],\n"
                + "          \"ports\": [\n"
                + "            {\n"
                + "              \"port\": 5701\n"
                + "            }\n"
                + "          ]\n"
                + "        }\n"
                + "      ]\n"
                + "    },\n"
                + "    {\n"
                + "      \"metadata\": {\n"
                + "        \"name\": \"service-1\"\n"
                + "      },\n"
                + "      \"subsets\": [\n"
                + "        {\n"
                + "          \"addresses\": [\n"
                + "            {\n"
                + "              \"ip\": \"172.17.0.5\",\n"
                + "              \"nodeName\": \"node-name-2\"\n"
                + "            }\n"
                + "          ],\n"
                + "          \"ports\": [\n"
                + "            {\n"
                + "              \"port\": 5702\n"
                + "            }\n"
                + "          ]\n"
                + "        }\n"
                + "      ]\n"
                + "    }\n"
                + "  ]\n"
                + "}";
    }

    private static String nodePortService1Response() {
        //language=JSON
        return "{\n"
        + "  \"kind\": \"Service\",\n"
        + "  \"spec\": {\n"
        + "    \"ports\": [\n"
        + "      {\n"
        + "        \"port\": 32123,\n"
        + "        \"targetPort\": 5701,\n"
        + "        \"nodePort\": 31916\n"
        + "      }\n"
        + "    ]\n"
        + "  }\n"
        + "}\n";
    }

    private static String nodePortService2Response() {
        //language=JSON
        return "{\n"
        + "  \"kind\": \"Service\",\n"
        + "  \"spec\": {\n"
        + "    \"ports\": [\n"
        + "      {\n"
        + "        \"port\": 32124,\n"
        + "        \"targetPort\": 5701,\n"
        + "        \"nodePort\": 31917\n"
        + "      }\n"
        + "    ]\n"
        + "  }\n"
        + "}";
    }

    @Test
    public void forbidden() {
        // given
        String forbiddenBody = "\"reason\":\"Forbidden\"";
        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), 403, forbiddenBody);

        // when
        List<Endpoint> result = kubernetesClient.endpoints();

        // then
        assertEquals(emptyList(), result);
    }

    @Test
    public void wrongApiToken() {
        // given
        String unauthorizedBody = "\"reason\":\"Unauthorized\"";
        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), 401, unauthorizedBody);

        // when
        List<Endpoint> result = kubernetesClient.endpoints();

        // then
        assertEquals(emptyList(), result);
    }

    @Test(expected = RestClientException.class)
    public void unknownException() {
        // given
        String notRetriedErrorBody = "\"reason\":\"Forbidden\"";
        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), 501, notRetriedErrorBody);

        // when
        kubernetesClient.endpoints();
    }

    private KubernetesClient newKubernetesClient(boolean useNodeNameAsExternalAddress) {
        String kubernetesMasterUrl = String.format("http://%s:%d", KUBERNETES_MASTER_IP, wireMockRule.port());
        return new KubernetesClient(NAMESPACE, kubernetesMasterUrl, TOKEN, CA_CERTIFICATE, RETRIES, useNodeNameAsExternalAddress);
    }

    private static List<String> format(List<Endpoint> addresses) {
        List<String> result = new ArrayList<String>();
        for (Endpoint address : addresses) {
            String ip = address.getPrivateAddress().getIp();
            Integer port = address.getPrivateAddress().getPort();
            boolean isReady = address.isReady();
            result.add(toString(ip, port, isReady));
        }
        return result;
    }

    private static List<String> formatPublic(List<Endpoint> addresses) {
        List<String> result = new ArrayList<String>();
        for (Endpoint address : addresses) {
            String ip = address.getPublicAddress().getIp();
            Integer port = address.getPublicAddress().getPort();
            boolean isReady = address.isReady();
            result.add(toString(ip, port, isReady));
        }
        return result;
    }

    private static void stub(String url, String response) {
        stub(url, 200, response);
    }

    private static void stub(String url, int status, String response) {
        stubFor(get(urlEqualTo(url))
                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
                .willReturn(aResponse().withStatus(status).withBody(response)));
    }

    private static void stub(String url, Map<String, String> queryParams, String response) {
        MappingBuilder mappingBuilder = get(urlPathMatching(url));
        for (String key : queryParams.keySet()) {
            mappingBuilder = mappingBuilder.withQueryParam(key, equalTo(queryParams.get(key)));
        }
        stubFor(mappingBuilder
                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
                .willReturn(aResponse().withStatus(200).withBody(response)));
    }

    private static String ready(String ip, Integer port) {
        return toString(ip, port, true);
    }

    private static String notReady(String ip, Integer port) {
        return toString(ip, port, false);
    }

    private static String toString(String ip, Integer port, boolean isReady) {
        return String.format("%s:%s:%s", ip, port, isReady);
    }
}