diff --git a/gradle.properties b/gradle.properties index 58d3b60..f839e99 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=5.0.0 +version=5.1.0 groupId=com.nike artifactId=cerberus-client diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 9a4637c..622ea02 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -38,13 +38,13 @@ dependencies { shadow "org.apache.commons:commons-lang3:3.4" shadow "org.slf4j:slf4j-api:1.7.25" shadow "com.google.code.gson:gson:2.5" - shadow "com.google.code.findbugs:jsr305:3.0.1" compile "com.squareup.okhttp3:okhttp:3.9.0" compile "org.apache.commons:commons-lang3:3.4" compile "com.google.code.gson:gson:2.5" - compile "com.google.code.findbugs:jsr305:3.0.1" compile "org.slf4j:slf4j-api:1.7.25" + compileOnly "com.google.code.findbugs:jsr305:3.0.1" + compileOnly 'com.google.code.findbugs:annotations:3.0.1' compile "com.amazonaws:aws-java-sdk-core:${AWS_SDK_VERSION}" compile "com.amazonaws:aws-java-sdk-kms:${AWS_SDK_VERSION}" diff --git a/src/integration/java/com/nike/cerberus/client/auth/aws/CerberusClientTest.java b/src/integration/java/com/nike/cerberus/client/auth/aws/CerberusClientTest.java index 8f53d40..5b6d066 100644 --- a/src/integration/java/com/nike/cerberus/client/auth/aws/CerberusClientTest.java +++ b/src/integration/java/com/nike/cerberus/client/auth/aws/CerberusClientTest.java @@ -18,15 +18,19 @@ import com.fieldju.commons.EnvUtils; import com.nike.cerberus.client.CerberusClient; +import com.nike.cerberus.client.CerberusServerApiException; import com.nike.cerberus.client.CerberusServerException; import com.nike.cerberus.client.DefaultCerberusUrlResolver; +import com.nike.cerberus.client.model.CerberusListFilesResponse; import com.nike.cerberus.client.model.CerberusListResponse; import com.nike.cerberus.client.model.CerberusResponse; import okhttp3.OkHttpClient; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.BeforeClass; import org.junit.Test; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -171,4 +175,54 @@ public void test_secret_is_deleted_after_auth_with_iam_principal_name() { } } + @Test + public void test_crud_for_files() { + + staticIamRoleCerberusCredentialsProvider = new StaticIamRoleCerberusCredentialsProvider( + new DefaultCerberusUrlResolver(), + iam_principal_arn, + region); + + cerberusClient = new CerberusClient(new DefaultCerberusUrlResolver(), + staticIamRoleCerberusCredentialsProvider, new OkHttpClient()); + + String fileContentStr = "file content string!"; + byte[] fileContentArr = fileContentStr.getBytes(StandardCharsets.UTF_8); + + // create file + cerberusClient.writeFile(sdbFullSecretPath, fileContentArr); + + // read file + byte[] file = cerberusClient.readFileAsBytes(sdbFullSecretPath); + String resultContentStr = new String(file, StandardCharsets.UTF_8); + assertEquals(fileContentStr, resultContentStr); + + // list files + CerberusListFilesResponse response = cerberusClient.listFiles(ROOT_SDB_PATH); + assertEquals( + StringUtils.substringAfter(sdbFullSecretPath, "/"), + response.getSecureFileSummaries().get(0).getPath() + ); + + // update file + String newFileContentStr = "new file content string*"; + byte[] newFileContentArr = newFileContentStr.getBytes(StandardCharsets.UTF_8); + cerberusClient.writeFile(sdbFullSecretPath, newFileContentArr); + + // confirm updated file data + byte[] updatedFileResult = cerberusClient.readFileAsBytes(sdbFullSecretPath); + String updatedFileStr = new String(updatedFileResult, StandardCharsets.UTF_8); + assertEquals(newFileContentStr, updatedFileStr); + + // delete file + cerberusClient.deleteFile(sdbFullSecretPath); + + // confirm file is deleted + try { + cerberusClient.readFileAsBytes(sdbFullSecretPath); + } catch (CerberusServerApiException cse) { + assertEquals(404, cse.getCode()); + } + } + } diff --git a/src/main/java/com/nike/cerberus/client/CerberusApiError.java b/src/main/java/com/nike/cerberus/client/CerberusApiError.java new file mode 100644 index 0000000..774923a --- /dev/null +++ b/src/main/java/com/nike/cerberus/client/CerberusApiError.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018 Nike, Inc. + * + * 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.nike.cerberus.client; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class CerberusApiError { + private int code; + private String message; + + public int getCode() { + return code; + } + + @SuppressFBWarnings("UWF_UNWRITTEN_FIELD") + public String getMessage() { + return message; + } + + @Override + public String toString() { + return String.format("ErrorCode: %s, Message: %s", code, message); + } +} diff --git a/src/main/java/com/nike/cerberus/client/CerberusClient.java b/src/main/java/com/nike/cerberus/client/CerberusClient.java index 3836969..fd1aae6 100644 --- a/src/main/java/com/nike/cerberus/client/CerberusClient.java +++ b/src/main/java/com/nike/cerberus/client/CerberusClient.java @@ -19,22 +19,29 @@ import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import com.nike.cerberus.client.auth.CerberusCredentialsProvider; import com.nike.cerberus.client.http.HttpHeader; import com.nike.cerberus.client.http.HttpMethod; import com.nike.cerberus.client.http.HttpStatus; +import com.nike.cerberus.client.model.CerberusListFilesResponse; import com.nike.cerberus.client.model.CerberusListResponse; import com.nike.cerberus.client.model.CerberusResponse; import okhttp3.Headers; import okhttp3.HttpUrl; import okhttp3.MediaType; +import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,6 +59,8 @@ public class CerberusClient { public static final String SECRET_PATH_PREFIX = "v1/secret/"; + public static final String SECURE_FILE_PATH_PREFIX = "v1/secure-file/"; + public static final MediaType DEFAULT_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8"); private final CerberusCredentialsProvider credentialsProvider; @@ -65,6 +74,13 @@ public class CerberusClient { private final Gson gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .disableHtmlEscaping() + .registerTypeAdapter(DateTime.class, new JsonDeserializer() { + @Override + public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return new DateTime(json.getAsString()); + } + }) .create(); private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -154,6 +170,51 @@ public CerberusListResponse list(final String path) { return gson.fromJson(gson.toJson(rootData.get("data")), CerberusListResponse.class); } + /** + * Lists all files at the specified path. Will return a {@link Map} that contains a paginated list + * of secure file summaries. If Cerberus returns an unexpected response code, a {@link CerberusServerException} + * will be thrown with the code and error details. If an unexpected I/O error is + * encountered, a {@link CerberusClientException} will be thrown wrapping the underlying exception. + *

+ * See https://www.github.com/Nike-Inc/cerberus-management-service/blob/master/API.md for details on what the + * list files operation returns. + *

+ * + * @param path Path to the data + * @return Cerberus response object that lists file metadata + */ + public CerberusListFilesResponse listFiles(final String path) { + return listFiles(path, null, null); + } + + /** + * Lists all files at the specified path. Will return a {@link Map} that contains a paginated list + * of secure file summaries. If Cerberus returns an unexpected response code, a {@link CerberusServerException} + * will be thrown with the code and error details. If an unexpected I/O error is + * encountered, a {@link CerberusClientException} will be thrown wrapping the underlying exception. + *

+ * See https://www.github.com/Nike-Inc/cerberus-management-service/blob/master/API.md for details on what the + * list files operation returns. + *

+ * + * @param path Path to the data + * @param limit The max number of results to return + * @param offset The number offset of results to return + * @return List of metadata for secure files at the specified path + */ + public CerberusListFilesResponse listFiles(final String path, Integer limit, Integer offset) { + final HttpUrl url = buildUrl("v1/secure-files/", path, limit, offset); + + logger.debug("list: requestUrl={}, limit={}, offset={}", url, limit, offset); + final Response response = execute(url, HttpMethod.GET, null); + + if (response.code() != HttpStatus.OK) { + parseAndThrowApiErrorResponse(response); + } + + return parseResponseBody(response, CerberusListFilesResponse.class); + } + /** * Read operation for a specified path. Will return a {@link Map} of the data stored at the specified path. * If Cerberus returns an unexpected response code, a {@link CerberusServerException} will be thrown with the code @@ -176,6 +237,28 @@ public CerberusResponse read(final String path) { return parseResponseBody(response, CerberusResponse.class); } + /** + * Read the binary contents of the file at the specified path. Will return the file contents stored at the specified path. + * If Cerberus returns an unexpected response code, a {@link CerberusServerException} will be thrown with the code + * and error details. If an unexpected I/O error is encountered, a {@link CerberusClientException} will be thrown + * wrapping the underlying exception. + * + * @param path Path to the data + * @return File contents + */ + public byte[] readFileAsBytes(final String path) { + final HttpUrl url = buildUrl(SECURE_FILE_PATH_PREFIX, path); + logger.debug("read: requestUrl={}", url); + + final Response response = execute(url, HttpMethod.GET, null); + + if (response.code() != HttpStatus.OK) { + parseAndThrowApiErrorResponse(response); + } + + return responseBodyAsBytes(response); + } + /** * Write operation for a specified path and data set. If Cerberus returns an unexpected response code, a * {@link CerberusServerException} will be thrown with the code and error details. If an unexpected I/O @@ -195,6 +278,58 @@ public void write(final String path, final Map data) { } } + /** + * Write operation for file at specified path with given content. If Cerberus returns an unexpected response code, a + * {@link CerberusServerException} will be thrown with the code and error details. If an unexpected I/O + * error is encountered, a {@link CerberusClientException} will be thrown wrapping the underlying exception. + * + * @param path Path for where to store the data + * @param contents File contents to be stored + */ + public void writeFile(final String path, final byte[] contents) { + final String fileName = StringUtils.substringAfterLast(path, "/"); + final HttpUrl url = buildUrl(SECURE_FILE_PATH_PREFIX, path); + logger.debug("write: requestUrl={}", url); + + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file-content", fileName, + RequestBody.create(MediaType.parse("application/octet-stream"), contents)) + .build(); + + Request request = new Request.Builder() + .url(url) + .headers(defaultHeaders) + .addHeader(HttpHeader.CERBERUS_TOKEN, credentialsProvider.getCredentials().getToken()) + .addHeader(HttpHeader.ACCEPT, DEFAULT_MEDIA_TYPE.toString()) + .post(requestBody) + .build(); + + final Response response = execute(request); + + if (response.code() != HttpStatus.NO_CONTENT) { + parseAndThrowApiErrorResponse(response); + } + } + + /** + * Delete operation for a file path. If Cerberus returns an unexpected response code, a + * {@link CerberusServerException} will be thrown with the code and error details. If an unexpected I/O + * error is encountered, a {@link CerberusClientException} will be thrown wrapping the underlying exception. + * + * @param path Path to file to be deleted + */ + public void deleteFile(final String path) { + final HttpUrl url = buildUrl(SECURE_FILE_PATH_PREFIX, path); + logger.debug("delete: requestUrl={}", url); + + final Response response = execute(url, HttpMethod.DELETE, null); + + if (response.code() != HttpStatus.NO_CONTENT) { + parseAndThrowApiErrorResponse(response); + } + } + /** * Delete operation for a specified path. If Cerberus returns an unexpected response code, a * {@link CerberusServerException} will be thrown with the code and error details. If an unexpected I/O @@ -249,6 +384,38 @@ public Headers getDefaultHeaders() { return defaultHeaders; } + /** + * Builds the full URL for preforming an operation against Cerberus. + * + * @param prefix Prefix between the environment URL and specified path + * @param path Path for the requested operation + * @param limit Limit of items to return in a paginated call + * @param offset Number offset of items in a paginated call + * @return Full URL to execute a request against + */ + protected HttpUrl buildUrl(final String prefix, + final String path, + final Integer limit, + final Integer offset) { + String baseUrl = urlResolver.resolve(); + baseUrl = StringUtils.appendIfMissing(baseUrl, "/"); + + final StringBuilder fullUrl = new StringBuilder() + .append(baseUrl) + .append(prefix) + .append(path); + + if (limit != null && offset != null) { + fullUrl.append("?limit=").append(limit).append("&offset=").append(offset); + } else if (limit != null) { + fullUrl.append("?limit=").append(limit); + } else if (offset != null) { + fullUrl.append("?offset=").append(offset); + } + + return HttpUrl.parse(fullUrl.toString()); + } + /** * Builds the full URL for preforming an operation against Cerberus. * @@ -290,6 +457,26 @@ protected Response execute(final HttpUrl url, final String method, final Object } } + /** + * Executes the HTTP request based on the input parameters. + * + * @param request The HTTP request to be made + * @return Response from the server + */ + protected Response execute(final Request request) { + try { + return httpClient.newCall(request).execute(); + } catch (IOException e) { + if (e instanceof SSLException + && e.getMessage() != null + && e.getMessage().contains("Unrecognized SSL message, plaintext connection?")) { + throw new CerberusClientException("I/O error while communicating with Cerberus. Unrecognized SSL message may be due to a web proxy e.g. AnyConnect", e); + } else { + throw new CerberusClientException("I/O error while communicating with Cerberus.", e); + } + } + } + /** * Build the HTTP request to execute for the Cerberus Client * @param url The URL to execute the request against @@ -377,6 +564,46 @@ protected void parseAndThrowErrorResponse(final Response response) { } } + /** + * Convenience method for parsing the errors from the HTTP response and throwing a {@link CerberusServerApiException}. + * + * @param response Response to parses the error details from + */ + protected void parseAndThrowApiErrorResponse(final Response response) { + final String responseBodyStr = responseBodyAsString(response); + logger.debug("parseAndThrowApiErrorResponse: responseCode={}, requestUrl={}, response={}", + response.code(), response.request().url(), responseBodyStr); + + try { + ApiErrorResponse errorResponse = gson.fromJson(responseBodyStr, ApiErrorResponse.class); + + if (errorResponse != null) { + throw new CerberusServerApiException(response.code(), errorResponse.getErrorId(), errorResponse.getErrors()); + } else { + throw new CerberusServerApiException(response.code(), null, new LinkedList()); + } + } catch (JsonSyntaxException e) { + logger.error("ERROR Failed to parse error message, response body received: {}", responseBodyStr); + throw new CerberusClientException("Error parsing the error response body from Cerberus, response code: " + response.code(), e); + } + } + + /** + * POJO for representing error response body from Cerberus. + */ + protected static class ApiErrorResponse { + private String errorId; + private List errors; + + public List getErrors() { + return errors; + } + + public String getErrorId() { + return errorId; + } + } + /** * POJO for representing error response body from Cerberus. */ @@ -396,4 +623,13 @@ protected String responseBodyAsString(Response response) { return "ERROR failed to print response body as str: " + ioe.getMessage(); } } + + protected byte[] responseBodyAsBytes(Response response) { + try { + return response.body().bytes(); + } catch (IOException ioe) { + logger.debug("responseBodyAsString: response={}", gson.toJson(response)); + throw new CerberusClientException("ERROR failed to print "); + } + } } diff --git a/src/main/java/com/nike/cerberus/client/CerberusServerApiException.java b/src/main/java/com/nike/cerberus/client/CerberusServerApiException.java new file mode 100644 index 0000000..1dd6515 --- /dev/null +++ b/src/main/java/com/nike/cerberus/client/CerberusServerApiException.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018 Nike, Inc. + * + * 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.nike.cerberus.client; + +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +public class CerberusServerApiException extends CerberusClientException { + private static final String MESSAGE_FORMAT = "Error ID: %s, Response Code: %s, Errors: [%s]"; + private static final long serialVersionUID = -8277885830842434365L; + + private final int code; + + private final String errorId; + + private final List errors; + + /** + * Construction of the exception with the specified code and error message list. + * + * @param code HTTP response code + * @param errorId Error ID + * @param errors List of error messages + */ + public CerberusServerApiException(final int code, final String errorId, final List errors) { + super(String.format(MESSAGE_FORMAT, errorId, code, StringUtils.join(errors, ", "))); + this.code = code; + this.errorId = errorId; + this.errors = errors; + } + + /** + * Returns the HTTP response code + * + * @return HTTP response code + */ + public int getCode() { + return code; + } + + /** + * Returns the error ID from Cerberus + * @return Error ID + */ + public String getErrorId() { + return errorId; + } + + /** + * Returns the list of error messages. + * + * @return Error messages + */ + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/com/nike/cerberus/client/model/CerberusListFilesResponse.java b/src/main/java/com/nike/cerberus/client/model/CerberusListFilesResponse.java new file mode 100644 index 0000000..87c9fbb --- /dev/null +++ b/src/main/java/com/nike/cerberus/client/model/CerberusListFilesResponse.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018 Nike, Inc. + * + * 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.nike.cerberus.client.model; + +import java.util.List; + +public class CerberusListFilesResponse { + + private boolean hasNext = false; + private Integer nextOffset = null; + private int limit = 0; + private int offset = 0; + private int fileCountInResult; + private int totalFileCount; + private List secureFileSummaries; + + public boolean isHasNext() { + return hasNext; + } + + public CerberusListFilesResponse setHasNext(boolean hasNext) { + this.hasNext = hasNext; + return this; + } + + public Integer getNextOffset() { + return nextOffset; + } + + public CerberusListFilesResponse setNextOffset(Integer nextOffset) { + this.nextOffset = nextOffset; + return this; + } + + public int getLimit() { + return limit; + } + + public CerberusListFilesResponse setLimit(int limit) { + this.limit = limit; + return this; + } + + public int getOffset() { + return offset; + } + + public CerberusListFilesResponse setOffset(int offset) { + this.offset = offset; + return this; + } + + public int getFileCountInResult() { + return fileCountInResult; + } + + public CerberusListFilesResponse setFileCountInResult(int fileCountInResult) { + this.fileCountInResult = fileCountInResult; + return this; + } + + public int getTotalFileCount() { + return totalFileCount; + } + + public CerberusListFilesResponse setTotalFileCount(int totalFileCount) { + this.totalFileCount = totalFileCount; + return this; + } + + public List getSecureFileSummaries() { + return secureFileSummaries; + } + + public CerberusListFilesResponse setSecureFileSummaries(List secureFileSummaries) { + this.secureFileSummaries = secureFileSummaries; + return this; + } +} diff --git a/src/main/java/com/nike/cerberus/client/model/SecureFileSummary.java b/src/main/java/com/nike/cerberus/client/model/SecureFileSummary.java new file mode 100644 index 0000000..cd3348a --- /dev/null +++ b/src/main/java/com/nike/cerberus/client/model/SecureFileSummary.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2018 Nike, Inc. + * + * 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.nike.cerberus.client.model; + +import org.joda.time.DateTime; + +public class SecureFileSummary { + + private String sdboxId; + private String path; + private int sizeInBytes; + private String name; + private String createdBy; + private DateTime createdTs; + private String lastUpdatedBy; + private DateTime lastUpdatedTs; + + public String getSdboxId() { + return sdboxId; + } + + public SecureFileSummary setSdboxId(String sdboxId) { + this.sdboxId = sdboxId; + return this; + } + + public String getPath() { + return path; + } + + public SecureFileSummary setPath(String path) { + this.path = path; + return this; + } + + public int getSizeInBytes() { + return sizeInBytes; + } + + public SecureFileSummary setSizeInBytes(int sizeInBytes) { + this.sizeInBytes = sizeInBytes; + return this; + } + + public String getName() { + return name; + } + + public SecureFileSummary setName(String name) { + this.name = name; + return this; + } + + public String getCreatedBy() { + return createdBy; + } + + public SecureFileSummary setCreatedBy(String createdBy) { + this.createdBy = createdBy; + return this; + } + + public DateTime getCreatedTs() { + return createdTs; + } + + public SecureFileSummary setCreatedTs(DateTime createdTs) { + this.createdTs = createdTs; + return this; + } + + public String getLastUpdatedBy() { + return lastUpdatedBy; + } + + public SecureFileSummary setLastUpdatedBy(String lastUpdatedBy) { + this.lastUpdatedBy = lastUpdatedBy; + return this; + } + + public DateTime getLastUpdatedTs() { + return lastUpdatedTs; + } + + public SecureFileSummary setLastUpdatedTs(DateTime lastUpdatedTs) { + this.lastUpdatedTs = lastUpdatedTs; + return this; + } +} diff --git a/src/test/java/com/nike/cerberus/client/CerberusClientTest.java b/src/test/java/com/nike/cerberus/client/CerberusClientTest.java index 2aa8fb3..fda4075 100644 --- a/src/test/java/com/nike/cerberus/client/CerberusClientTest.java +++ b/src/test/java/com/nike/cerberus/client/CerberusClientTest.java @@ -43,6 +43,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -259,6 +260,23 @@ public void build_request_includes_default_headers() throws IOException { assertThat(result.headers().get(headerKey)).isEqualTo(headerValue); } + @Test + public void buildUrl_properly_adds_limit_and_offset() { + String prefix = "prefix/"; + String path = "path"; + Integer limit = 1000; + Integer offset = 2; + HttpUrl urlWithNoLimitOrOffset = cerberusClient.buildUrl(prefix, path, null, null); + HttpUrl urlWithLimitAndNoOffset = cerberusClient.buildUrl(prefix, path, limit, null); + HttpUrl urlWithOffsetAndNoLimit = cerberusClient.buildUrl(prefix, path, null, offset); + HttpUrl urlWithLimitAndOffset = cerberusClient.buildUrl(prefix, path, limit, offset); + + assertTrue(urlWithNoLimitOrOffset.toString().endsWith(String.format("%s%s", prefix, path))); + assertTrue(urlWithLimitAndNoOffset.toString().endsWith(String.format("%s%s?limit=%s", prefix, path, limit))); + assertTrue(urlWithOffsetAndNoLimit.toString().endsWith(String.format("%s%s?offset=%s", prefix, path, offset))); + assertTrue(urlWithLimitAndOffset.toString().endsWith(String.format("%s%s?limit=%s&offset=%s", prefix, path, limit, offset))); + } + private OkHttpClient buildHttpClient(int timeout, TimeUnit timeoutUnit) { return new OkHttpClient.Builder() .connectTimeout(timeout, timeoutUnit)