diff --git a/src/main/java/io/github/microcks/testcontainers/MicrocksContainer.java b/src/main/java/io/github/microcks/testcontainers/MicrocksContainer.java index fb70324..174a5b9 100644 --- a/src/main/java/io/github/microcks/testcontainers/MicrocksContainer.java +++ b/src/main/java/io/github/microcks/testcontainers/MicrocksContainer.java @@ -15,6 +15,7 @@ */ package io.github.microcks.testcontainers; +import io.github.microcks.testcontainers.model.DailyInvocationStatistic; import io.github.microcks.testcontainers.model.RequestResponsePair; import io.github.microcks.testcontainers.model.Secret; import io.github.microcks.testcontainers.model.TestResult; @@ -30,6 +31,7 @@ import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import org.testcontainers.shaded.com.github.dockerjava.core.MediaType; import org.testcontainers.shaded.com.google.common.net.HttpHeaders; +import org.testcontainers.shaded.com.google.common.net.UrlEscapers; import org.testcontainers.shaded.org.awaitility.Awaitility; import org.testcontainers.shaded.org.awaitility.core.ConditionTimeoutException; import org.testcontainers.utility.DockerImageName; @@ -43,6 +45,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLDecoder; @@ -561,6 +564,73 @@ public void downloadAsSecondaryRemoteArtifact(String remoteArtifactUrl) throws A downloadArtifact(remoteArtifactUrl, false); } + /** + * Verifies that given Service has been invoked at least one time. + * @param serviceName + * @param serviceVersion + * @return + */ + public boolean verify(String serviceName, String serviceVersion) { + DailyInvocationStatistic dailyInvocationStatistic = getServiceInvocations(getHttpEndpoint(), serviceName, serviceVersion); + + if (dailyInvocationStatistic == null) { + return false; + } + + BigDecimal count = dailyInvocationStatistic.getDailyCount(); + + return count != null && count.intValue() != 0; + } + + /** + * Get the invocations' count for a given service name and version. + * @param serviceName + * @param serviceVersion + * @return + */ + public Long getServiceInvocationsCount(String serviceName, String serviceVersion) { + DailyInvocationStatistic dailyInvocationStatistic = getServiceInvocations(getHttpEndpoint(), serviceName, serviceVersion); + + if (dailyInvocationStatistic == null) { + return 0L; + } + + BigDecimal count = dailyInvocationStatistic.getDailyCount(); + + return count.longValue(); + } + + + /** + * + * @param microcksContainerHttpEndpoint + * @param serviceName + * @param serviceVersion + * @return + */ + private static DailyInvocationStatistic getServiceInvocations(String microcksContainerHttpEndpoint, String serviceName, String serviceVersion) { + String encodedServiceName = UrlEscapers.urlFragmentEscaper().escape(serviceName); + String encodedServiceVersion = UrlEscapers.urlFragmentEscaper().escape(serviceVersion); + + String restApiURL = microcksContainerHttpEndpoint + "/api/metrics/invocations/" + encodedServiceName + "/" + encodedServiceVersion; + + try { + Thread.sleep(100); // to avoid race condition issue when requesting Microcks Metrics REST API + } catch (InterruptedException e) { + log.warn("Failed to sleep before calling Microcks API", e); + Thread.currentThread().interrupt(); + } + + try { + StringBuilder content = getFromRestApi(restApiURL); + return content.length() == 0 ? null : getMapper().readValue(content.toString(), DailyInvocationStatistic.class); + } catch (IOException e) { + log.warn("Failed to get service's invocations at address " + restApiURL, e); + } + + return null; + } + private void importArtifact(String artifactPath, boolean mainArtifact) { URL resource = Thread.currentThread().getContextClassLoader().getResource(artifactPath); if (resource == null) { @@ -727,8 +797,20 @@ private static HttpURLConnection uploadFileToMicrocks(URL microcksApiURL, File f } private static TestResult refreshTestResult(String microcksContainerHttpEndpoint, String testResultId) throws IOException { + StringBuilder content = getFromRestApi(microcksContainerHttpEndpoint + "/api/tests/" + testResultId); + + return getMapper().readValue(content.toString(), TestResult.class); + } + + /** + * + * @param restApiURL + * @return + * @throws IOException + */ + private static StringBuilder getFromRestApi(String restApiURL) throws IOException { // Build a new client on correct API endpoint. - URL url = new URL(microcksContainerHttpEndpoint + "/api/tests/" + testResultId); + URL url = new URL(restApiURL); HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); httpConn.setRequestMethod("GET"); httpConn.setRequestProperty("Accept", "application/json"); @@ -742,10 +824,10 @@ private static TestResult refreshTestResult(String microcksContainerHttpEndpoint content.append(inputLine); } } + // Disconnect Http connection. httpConn.disconnect(); - - return getMapper().readValue(content.toString(), TestResult.class); + return content; } public static class ArtifactLoadException extends RuntimeException { diff --git a/src/main/java/io/github/microcks/testcontainers/model/DailyInvocationStatistic.java b/src/main/java/io/github/microcks/testcontainers/model/DailyInvocationStatistic.java new file mode 100644 index 0000000..1ad579d --- /dev/null +++ b/src/main/java/io/github/microcks/testcontainers/model/DailyInvocationStatistic.java @@ -0,0 +1,263 @@ +/* + * Microcks API v1.10 + * API offered by Microcks, the Kubernetes native tool for API and microservices mocking and testing (microcks.io) + * + * The version of the OpenAPI document: 1.10.0 + * Contact: laurent@microcks.io + * + * NOTE: This class comes from microcks-java-client https://github.com/microcks/microcks-java-client + */ +package io.github.microcks.testcontainers.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * The daily statistic of a service mock invocations + */ +public class DailyInvocationStatistic { + public static final String JSON_PROPERTY_ID = "id"; + private String id; + + public static final String JSON_PROPERTY_DAY = "day"; + private String day; + + public static final String JSON_PROPERTY_SERVICE_NAME = "serviceName"; + private String serviceName; + + public static final String JSON_PROPERTY_SERVICE_VERSION = "serviceVersion"; + private String serviceVersion; + + public static final String JSON_PROPERTY_DAILY_COUNT = "dailyCount"; + private BigDecimal dailyCount; + + public static final String JSON_PROPERTY_HOURLY_COUNT = "hourlyCount"; + private Map hourlyCount = new HashMap<>(); + + public static final String JSON_PROPERTY_MINUTE_COUNT = "minuteCount"; + private Map minuteCount = new HashMap<>(); + + public DailyInvocationStatistic() { + } + + public DailyInvocationStatistic id(String id) { + this.id = id; + return this; + } + + /** + * Unique identifier of this statistic object + * @return id + */ + @JsonProperty(JSON_PROPERTY_ID) + public String getId() { + return id; + } + + + @JsonProperty(JSON_PROPERTY_ID) + public void setId(String id) { + this.id = id; + } + + + public DailyInvocationStatistic day(String day) { + this.day = day; + return this; + } + + /** + * The day (formatted as yyyyMMdd string) represented by this statistic + * @return day + */ + @JsonProperty(JSON_PROPERTY_DAY) + public String getDay() { + return day; + } + + + @JsonProperty(JSON_PROPERTY_DAY) + public void setDay(String day) { + this.day = day; + } + + + public DailyInvocationStatistic serviceName(String serviceName) { + this.serviceName = serviceName; + return this; + } + + /** + * The name of the service this statistic is related to + * @return serviceName + */ + @JsonProperty(JSON_PROPERTY_SERVICE_NAME) + public String getServiceName() { + return serviceName; + } + + + @JsonProperty(JSON_PROPERTY_SERVICE_NAME) + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + + public DailyInvocationStatistic serviceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + return this; + } + + /** + * The version of the service this statistic is related to + * @return serviceVersion + */ + @JsonProperty(JSON_PROPERTY_SERVICE_VERSION) + public String getServiceVersion() { + return serviceVersion; + } + + + @JsonProperty(JSON_PROPERTY_SERVICE_VERSION) + public void setServiceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + } + + + public DailyInvocationStatistic dailyCount(BigDecimal dailyCount) { + this.dailyCount = dailyCount; + return this; + } + + /** + * The number of service mock invocations on this day + * @return dailyCount + */ + @JsonProperty(JSON_PROPERTY_DAILY_COUNT) + public BigDecimal getDailyCount() { + return dailyCount; + } + + + @JsonProperty(JSON_PROPERTY_DAILY_COUNT) + public void setDailyCount(BigDecimal dailyCount) { + this.dailyCount = dailyCount; + } + + + public DailyInvocationStatistic hourlyCount(Map hourlyCount) { + this.hourlyCount = hourlyCount; + return this; + } + + public DailyInvocationStatistic putHourlyCountItem(String key, Object hourlyCountItem) { + if (this.hourlyCount == null) { + this.hourlyCount = new HashMap<>(); + } + this.hourlyCount.put(key, hourlyCountItem); + return this; + } + + /** + * The number of service mock invocations per hour of the day (keys range from 0 to 23) + * @return hourlyCount + */ + @JsonProperty(JSON_PROPERTY_HOURLY_COUNT) + public Map getHourlyCount() { + return hourlyCount; + } + + + @JsonProperty(JSON_PROPERTY_HOURLY_COUNT) + public void setHourlyCount(Map hourlyCount) { + this.hourlyCount = hourlyCount; + } + + + public DailyInvocationStatistic minuteCount(Map minuteCount) { + this.minuteCount = minuteCount; + return this; + } + + public DailyInvocationStatistic putMinuteCountItem(String key, Object minuteCountItem) { + if (this.minuteCount == null) { + this.minuteCount = new HashMap<>(); + } + this.minuteCount.put(key, minuteCountItem); + return this; + } + + /** + * The number of service mock invocations per minute of the day (keys range from 0 to 1439) + * @return minuteCount + */ + @JsonProperty(JSON_PROPERTY_MINUTE_COUNT) + public Map getMinuteCount() { + return minuteCount; + } + + + @JsonProperty(JSON_PROPERTY_MINUTE_COUNT) + public void setMinuteCount(Map minuteCount) { + this.minuteCount = minuteCount; + } + + + /** + * Return true if this DailyInvocationStatistic object is equal to o. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DailyInvocationStatistic dailyInvocationStatistic = (DailyInvocationStatistic) o; + return Objects.equals(this.id, dailyInvocationStatistic.id) && + Objects.equals(this.day, dailyInvocationStatistic.day) && + Objects.equals(this.serviceName, dailyInvocationStatistic.serviceName) && + Objects.equals(this.serviceVersion, dailyInvocationStatistic.serviceVersion) && + Objects.equals(this.dailyCount, dailyInvocationStatistic.dailyCount) && + Objects.equals(this.hourlyCount, dailyInvocationStatistic.hourlyCount) && + Objects.equals(this.minuteCount, dailyInvocationStatistic.minuteCount); + } + + @Override + public int hashCode() { + return Objects.hash(id, day, serviceName, serviceVersion, dailyCount, hourlyCount, minuteCount); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class DailyInvocationStatistic {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" day: ").append(toIndentedString(day)).append("\n"); + sb.append(" serviceName: ").append(toIndentedString(serviceName)).append("\n"); + sb.append(" serviceVersion: ").append(toIndentedString(serviceVersion)).append("\n"); + sb.append(" dailyCount: ").append(toIndentedString(dailyCount)).append("\n"); + sb.append(" hourlyCount: ").append(toIndentedString(hourlyCount)).append("\n"); + sb.append(" minuteCount: ").append(toIndentedString(minuteCount)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + + diff --git a/src/test/java/io/github/microcks/testcontainers/MicrocksContainerTest.java b/src/test/java/io/github/microcks/testcontainers/MicrocksContainerTest.java index c9d0cea..3bff8b4 100644 --- a/src/test/java/io/github/microcks/testcontainers/MicrocksContainerTest.java +++ b/src/test/java/io/github/microcks/testcontainers/MicrocksContainerTest.java @@ -222,6 +222,9 @@ private void testMockEndpoints(MicrocksContainer microcks) { private void testMicrocksMockingFunctionality(MicrocksContainer microcks) { String baseApiUrl = microcks.getRestMockEndpoint("API Pastries", "0.0.1"); + assertFalse(microcks.verify("API Pastries", "0.0.1")); + assertEquals(0, microcks.getServiceInvocationsCount("API Pastries", "0.0.1")); + // Check that mock from main/primary artifact has been loaded. Response millefeuille = RestAssured.given().when() .get(baseApiUrl + "/pastries/Millefeuille") @@ -230,6 +233,8 @@ private void testMicrocksMockingFunctionality(MicrocksContainer microcks) { assertEquals(200, millefeuille.getStatusCode()); assertEquals("Millefeuille", millefeuille.jsonPath().get("name")); //millefeuille.getBody().prettyPrint(); + assertTrue(microcks.verify("API Pastries", "0.0.1")); + assertEquals(1, microcks.getServiceInvocationsCount("API Pastries", "0.0.1")); // Check that mock from secondary artifact has been loaded. Response eclairChocolat = RestAssured.given().when() @@ -239,6 +244,8 @@ private void testMicrocksMockingFunctionality(MicrocksContainer microcks) { assertEquals(200, eclairChocolat.getStatusCode()); assertEquals("Eclair Chocolat", eclairChocolat.jsonPath().get("name")); //eclairChocolat.getBody().prettyPrint(); + assertTrue(microcks.verify("API Pastries", "0.0.1")); + assertEquals(2, microcks.getServiceInvocationsCount("API Pastries", "0.0.1")); // Check that mock from main/primary remote artifact has been loaded. baseApiUrl = microcks.getRestMockEndpoint("API Pastry - 2.0", "2.0.0"); @@ -250,6 +257,9 @@ private void testMicrocksMockingFunctionality(MicrocksContainer microcks) { assertEquals(200, millefeuille.getStatusCode()); assertEquals("Millefeuille", millefeuille.jsonPath().get("name")); //millefeuille.getBody().prettyPrint(); + + assertTrue(microcks.verify("API Pastry - 2.0", "2.0.0")); + assertEquals(1, microcks.getServiceInvocationsCount("API Pastry - 2.0", "2.0.0")); } private void testMicrocksContractTestingFunctionality(MicrocksContainer microcks, GenericContainer badImpl, GenericContainer goodImpl) throws Exception {