diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index 86eac2ea1..e34d0c5eb 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -331,6 +331,75 @@ protected Template execute() throws FirebaseRemoteConfigException { }; } + /** + * Gets a list of Remote Config template versions that have been published, sorted in reverse + * chronological order. Only the last 300 versions are stored. + * + *

All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * + * @return A {@link ListVersionsPage} instance. + * @throws FirebaseRemoteConfigException If an error occurs while retrieving versions list. + */ + public ListVersionsPage listVersions() throws FirebaseRemoteConfigException { + return listVersionsOp().call(); + } + + /** + * Gets a list of Remote Config template versions that have been published, sorted in reverse + * chronological order. Only the last 300 versions are stored. + * + *

All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * + * @param options List version options. + * @return A {@link ListVersionsPage} instance. + * @throws FirebaseRemoteConfigException If an error occurs while retrieving versions list. + */ + public ListVersionsPage listVersions( + @NonNull ListVersionsOptions options) throws FirebaseRemoteConfigException { + return listVersionsOp(options).call(); + } + + /** + * Similar to {@link #listVersions()} but performs the operation + * asynchronously. + * + * @return A {@link ListVersionsPage} instance. + */ + public ApiFuture listVersionsAsync() { + return listVersionsOp().callAsync(app); + } + + /** + * Similar to {@link #listVersions(ListVersionsOptions options)} but performs the operation + * asynchronously. + * + * @param options List version options. + * @return A {@link ListVersionsPage} instance. + */ + public ApiFuture listVersionsAsync(@NonNull ListVersionsOptions options) { + return listVersionsOp(options).callAsync(app); + } + + private CallableOperation listVersionsOp() { + return listVersionsOp(null); + } + + private CallableOperation listVersionsOp( + final ListVersionsOptions options) { + final FirebaseRemoteConfigClient remoteConfigClient = getRemoteConfigClient(); + final ListVersionsPage.DefaultVersionSource source = + new ListVersionsPage.DefaultVersionSource(remoteConfigClient); + final ListVersionsPage.Factory factory = new ListVersionsPage.Factory(source, options); + return new CallableOperation() { + @Override + protected ListVersionsPage execute() throws FirebaseRemoteConfigException { + return factory.create(); + } + }; + } + @VisibleForTesting FirebaseRemoteConfigClient getRemoteConfigClient() { return remoteConfigClient; diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java index fba0bfd58..9fdb596d6 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java @@ -16,6 +16,8 @@ package com.google.firebase.remoteconfig; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ListVersionsResponse; + /** * An interface for managing Firebase Remote Config templates. */ @@ -35,4 +37,7 @@ Template publishTemplate(Template template, boolean validateOnly, boolean forcePublish) throws FirebaseRemoteConfigException; Template rollback(String versionNumber) throws FirebaseRemoteConfigException; + + ListVersionsResponse listVersions( + ListVersionsOptions options) throws FirebaseRemoteConfigException; } diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java index 824d91a14..aff5439ac 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java @@ -105,7 +105,7 @@ public Template getTemplate() throws FirebaseRemoteConfigException { @Override public Template getTemplateAtVersion( @NonNull String versionNumber) throws FirebaseRemoteConfigException { - checkArgument(isValidVersionNumber(versionNumber), + checkArgument(RemoteConfigUtil.isValidVersionNumber(versionNumber), "Version number must be a non-empty string in int64 format."); HttpRequestInfo request = HttpRequestInfo.buildGetRequest(remoteConfigUrl) .addAllHeaders(COMMON_HEADERS) @@ -141,7 +141,7 @@ public Template publishTemplate(@NonNull Template template, boolean validateOnly @Override public Template rollback(@NonNull String versionNumber) throws FirebaseRemoteConfigException { - checkArgument(isValidVersionNumber(versionNumber), + checkArgument(RemoteConfigUtil.isValidVersionNumber(versionNumber), "Version number must be a non-empty string in int64 format."); Map content = ImmutableMap.of("versionNumber", versionNumber); HttpRequestInfo request = HttpRequestInfo @@ -153,6 +153,17 @@ public Template rollback(@NonNull String versionNumber) throws FirebaseRemoteCon return template.setETag(getETag(response)); } + @Override + public TemplateResponse.ListVersionsResponse listVersions( + ListVersionsOptions options) throws FirebaseRemoteConfigException { + HttpRequestInfo request = HttpRequestInfo.buildGetRequest(remoteConfigUrl + ":listVersions") + .addAllHeaders(COMMON_HEADERS); + if (options != null) { + request.addAllParameters(options.wrapForTransport()); + } + return httpClient.sendAndParse(request, TemplateResponse.ListVersionsResponse.class); + } + private String getETag(IncomingHttpResponse response) { List etagList = (List) response.getHeaders().get("etag"); checkState(etagList != null && !etagList.isEmpty(), @@ -165,10 +176,6 @@ private String getETag(IncomingHttpResponse response) { return etag; } - private boolean isValidVersionNumber(String versionNumber) { - return !Strings.isNullOrEmpty(versionNumber) && versionNumber.matches("^\\d+$"); - } - static FirebaseRemoteConfigClientImpl fromApp(FirebaseApp app) { String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), diff --git a/src/main/java/com/google/firebase/remoteconfig/ListVersionsOptions.java b/src/main/java/com/google/firebase/remoteconfig/ListVersionsOptions.java new file mode 100644 index 000000000..be7a62ea5 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ListVersionsOptions.java @@ -0,0 +1,198 @@ +/* + * Copyright 2020 Google LLC + * + * 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.google.firebase.remoteconfig; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +/** + * A class representing options for Remote Config list versions operation. + */ +public final class ListVersionsOptions { + + private final Integer pageSize; + private final String pageToken; + private final String endVersionNumber; + private final String startTime; + private final String endTime; + + private ListVersionsOptions(Builder builder) { + if (builder.pageSize != null) { + checkArgument(builder.pageSize > 0 && builder.pageSize < 301, + "pageSize must be a number between 1 and 300 (inclusive)."); + } + if (builder.endVersionNumber != null) { + checkArgument(RemoteConfigUtil.isValidVersionNumber(builder.endVersionNumber) + && (Integer.parseInt(builder.endVersionNumber) > 0), + "endVersionNumber must be a non-empty string in int64 format and must be" + + " greater than 0."); + } + this.pageSize = builder.pageSize; + this.pageToken = builder.pageToken; + this.endVersionNumber = builder.endVersionNumber; + this.startTime = builder.startTime; + this.endTime = builder.endTime; + } + + Map wrapForTransport() { + Map optionsMap = new HashMap<>(); + if (this.pageSize != null) { + optionsMap.put("pageSize", this.pageSize); + } + if (this.pageToken != null) { + optionsMap.put("pageToken", this.pageToken); + } + if (this.endVersionNumber != null) { + optionsMap.put("endVersionNumber", this.endVersionNumber); + } + if (this.startTime != null) { + optionsMap.put("startTime", this.startTime); + } + if (this.endTime != null) { + optionsMap.put("endTime", this.endTime); + } + return optionsMap; + } + + String getPageToken() { + return pageToken; + } + + /** + * Creates a new {@link ListVersionsOptions.Builder}. + * + * @return A {@link ListVersionsOptions.Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new {@code Builder} from the options object. + * + *

The new builder is not backed by this object's values; that is, changes made to the new + * builder don't change the values of the origin object. + */ + public Builder toBuilder() { + return new Builder(this); + } + + public static class Builder { + private Integer pageSize; + private String pageToken; + private String endVersionNumber; + private String startTime; + private String endTime; + + private Builder() {} + + private Builder(ListVersionsOptions options) { + this.pageSize = options.pageSize; + this.pageToken = options.pageToken; + this.endVersionNumber = options.endVersionNumber; + this.startTime = options.startTime; + this.endTime = options.endTime; + } + + /** + * Sets the page size. + * + * @param pageSize The maximum number of items to return per page. + * @return This builder. + */ + public Builder setPageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + + /** + * Sets the page token. + * + * @param pageToken The {@code nextPageToken} value returned from a previous List request, + * if any. + * @return This builder. + */ + public Builder setPageToken(String pageToken) { + this.pageToken = pageToken; + return this; + } + + /** + * Sets the newest version number to include in the results. + * + * @param endVersionNumber Specify the newest version number to include in the results. + * If specified, must be greater than zero. Defaults to the newest + * version. + * @return This builder. + */ + public Builder setEndVersionNumber(String endVersionNumber) { + this.endVersionNumber = endVersionNumber; + return this; + } + + /** + * Sets the newest version number to include in the results. + * + * @param endVersionNumber Specify the newest version number to include in the results. + * If specified, must be greater than zero. Defaults to the newest + * version. + * @return This builder. + */ + public Builder setEndVersionNumber(long endVersionNumber) { + this.endVersionNumber = String.valueOf(endVersionNumber);; + return this; + } + + /** + * Sets the earliest update time to include in the results. + * + * @param startTimeMillis Specify the earliest update time to include in the results. + * Any entries updated before this time are omitted. + * @return This builder. + */ + public Builder setStartTimeMillis(long startTimeMillis) { + this.startTime = RemoteConfigUtil.convertToUtcZuluFormat(startTimeMillis); + return this; + } + + /** + * Sets the latest update time to include in the results. + * + * @param endTimeMillis Specify the latest update time to include in the results. + * Any entries updated on or after this time are omitted. + * @return This builder. + */ + public Builder setEndTimeMillis(long endTimeMillis) { + this.endTime = RemoteConfigUtil.convertToUtcZuluFormat(endTimeMillis); + return this; + } + + /** + * Builds a new {@link ListVersionsOptions} instance from the fields set on this builder. + * + * @return A non-null {@link ListVersionsOptions}. + */ + public ListVersionsOptions build() { + return new ListVersionsOptions(this); + } + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/ListVersionsPage.java b/src/main/java/com/google/firebase/remoteconfig/ListVersionsPage.java new file mode 100644 index 000000000..22431f716 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ListVersionsPage.java @@ -0,0 +1,268 @@ +/* + * Copyright 2020 Google LLC + * + * 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.google.firebase.remoteconfig; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.gax.paging.Page; +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.TemplateResponse; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Represents a page of {@link Version} instances. Provides methods for iterating + * over the versions in the current page, and calling up subsequent pages of versions. Instances of + * this class are thread-safe and immutable. + */ +public final class ListVersionsPage implements Page { + + static final String END_OF_LIST = ""; + + private final VersionsResultBatch currentBatch; + private final VersionSource source; + private final ListVersionsOptions listVersionsOptions; + + private ListVersionsPage( + @NonNull VersionsResultBatch currentBatch, @NonNull VersionSource source, + @NonNull ListVersionsOptions listVersionsOptions) { + this.currentBatch = checkNotNull(currentBatch); + this.source = checkNotNull(source); + this.listVersionsOptions = listVersionsOptions; + } + + /** + * Checks if there is another page of versions available to retrieve. + * + * @return true if another page is available, or false otherwise. + */ + @Override + public boolean hasNextPage() { + return !END_OF_LIST.equals(currentBatch.getNextPageToken()); + } + + /** + * Returns the next page of versions. + * + * @return A new {@link ListVersionsPage} instance, or null if there are no more pages. + */ + @NonNull + @Override + public ListVersionsPage getNextPage() { + if (hasNextPage()) { + ListVersionsOptions options; + if (listVersionsOptions != null) { + options = listVersionsOptions.toBuilder().setPageToken(currentBatch.getNextPageToken()) + .build(); + } else { + options = ListVersionsOptions.builder().setPageToken(currentBatch.getNextPageToken()) + .build(); + } + Factory factory = new Factory(source, options); + try { + return factory.create(); + } catch (FirebaseRemoteConfigException e) { + throw new RuntimeException(e); + } + } + return null; + } + + /** + * Returns the string token that identifies the next page. Never returns null. Returns empty + * string if there are no more pages available to be retrieved. + * + * @return A non-null string token (possibly empty, representing no more pages) + */ + @NonNull + @Override + public String getNextPageToken() { + return currentBatch.getNextPageToken(); + } + + /** + * Returns an {@code Iterable} over the versions in this page. + * + * @return a {@code Iterable} instance. + */ + @NonNull + @Override + public Iterable getValues() { + return currentBatch.getVersions(); + } + + /** + * Returns an {@code Iterable} that facilitates transparently iterating over all the versions in + * the current Firebase project, starting from this page. The {@code Iterator} instances produced + * by the returned {@code Iterable} never buffers more than one page of versions at a time. It is + * safe to abandon the iterators (i.e. break the loops) at any time. + * + * @return a new {@code Iterable} instance. + */ + @NonNull + @Override + public Iterable iterateAll() { + return new VersionIterable(this); + } + + private static class VersionIterable implements Iterable { + + private final ListVersionsPage startingPage; + + VersionIterable(@NonNull ListVersionsPage startingPage) { + this.startingPage = checkNotNull(startingPage, "starting page must not be null"); + } + + @Override + @NonNull + public Iterator iterator() { + return new VersionIterable.VersionIterator(startingPage); + } + + /** + * An {@code Iterator} that cycles through versions, one at a time. It buffers the + * last retrieved batch of versions in memory. + */ + private static class VersionIterator implements Iterator { + + private ListVersionsPage currentPage; + private List batch; + private int index = 0; + + private VersionIterator(ListVersionsPage startingPage) { + setCurrentPage(startingPage); + } + + @Override + public boolean hasNext() { + if (index == batch.size()) { + if (currentPage.hasNextPage()) { + setCurrentPage(currentPage.getNextPage()); + } else { + return false; + } + } + + return index < batch.size(); + } + + @Override + public Version next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return batch.get(index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove operation not supported"); + } + + private void setCurrentPage(ListVersionsPage page) { + this.currentPage = checkNotNull(page); + this.batch = ImmutableList.copyOf(page.getValues()); + this.index = 0; + } + } + } + + static final class VersionsResultBatch { + + private final List versions; + private final String nextPageToken; + + VersionsResultBatch(@NonNull List versions, @NonNull String nextPageToken) { + this.versions = checkNotNull(versions); + this.nextPageToken = checkNotNull(nextPageToken); // Can be empty + } + + @NonNull + List getVersions() { + return versions; + } + + @NonNull + String getNextPageToken() { + return nextPageToken; + } + } + + /** + * Represents a source of Remote Config version data that can be queried to load a batch + * of versions. + */ + interface VersionSource { + @NonNull + VersionsResultBatch fetch( + ListVersionsOptions listVersionsOptions) throws FirebaseRemoteConfigException; + } + + static class DefaultVersionSource implements VersionSource { + + private final FirebaseRemoteConfigClient remoteConfigClient; + + DefaultVersionSource(FirebaseRemoteConfigClient remoteConfigClient) { + this.remoteConfigClient = checkNotNull(remoteConfigClient, + "remote config client must not be null"); + } + + @Override + public VersionsResultBatch fetch( + ListVersionsOptions listVersionsOptions) throws FirebaseRemoteConfigException { + TemplateResponse.ListVersionsResponse response = remoteConfigClient + .listVersions(listVersionsOptions); + ImmutableList.Builder builder = ImmutableList.builder(); + if (response.hasVersions()) { + for (TemplateResponse.VersionResponse versionResponse : response.getVersions()) { + builder.add(new Version(versionResponse)); + } + } + String nextPageToken = response.getNextPageToken() != null + ? response.getNextPageToken() : END_OF_LIST; + return new VersionsResultBatch(builder.build(), nextPageToken); + } + } + + /** + * A simple factory class for {@link ListVersionsPage} instances. Performs argument validation + * before attempting to load any version data (which is expensive, and hence may be performed + * asynchronously on a separate thread). + */ + static class Factory { + + private final VersionSource source; + private final ListVersionsOptions listVersionsOptions; + + Factory(@NonNull VersionSource source) { + this(source, null); + } + + Factory(@NonNull VersionSource source, @NonNull ListVersionsOptions listVersionsOptions) { + this.source = checkNotNull(source, "source must not be null"); + this.listVersionsOptions = listVersionsOptions; + } + + ListVersionsPage create() throws FirebaseRemoteConfigException { + VersionsResultBatch batch = source.fetch(listVersionsOptions); + return new ListVersionsPage(batch, source, listVersionsOptions); + } + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java index 6aa94027c..6d794503f 100644 --- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigErrorCode.java @@ -30,4 +30,31 @@ public enum RemoteConfigErrorCode { * Internal server error. */ INTERNAL, + + /** + * Request cannot be executed in the current system state, such as deleting a non-empty + * directory. + */ + FAILED_PRECONDITION, + + /** + * User is not authenticated. + */ + UNAUTHENTICATED, + + /** + * The resource that a client tried to create already exists. + */ + ALREADY_EXISTS, + + /** + * Failed to validate Remote Config data. + */ + VALIDATION_ERROR, + + /** + * The current version specified in an update request + * did not match the actual version in the database. + */ + VERSION_MISMATCH, } diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java new file mode 100644 index 000000000..13a49076a --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Google LLC + * + * 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.google.firebase.remoteconfig; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +final class RemoteConfigUtil { + + static boolean isValidVersionNumber(String versionNumber) { + return !Strings.isNullOrEmpty(versionNumber) && versionNumber.matches("^\\d+$"); + } + + static String convertToUtcZuluFormat(long millis) { + checkArgument(millis >= 0, "Milliseconds duration must not be negative"); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.format(new Date(millis)); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java index 0c517e14e..050add953 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/RemoteConfigServiceErrorResponse.java @@ -38,6 +38,11 @@ public final class RemoteConfigServiceErrorResponse extends GenericJson { ImmutableMap.builder() .put("INTERNAL", RemoteConfigErrorCode.INTERNAL) .put("INVALID_ARGUMENT", RemoteConfigErrorCode.INVALID_ARGUMENT) + .put("FAILED_PRECONDITION", RemoteConfigErrorCode.FAILED_PRECONDITION) + .put("UNAUTHENTICATED", RemoteConfigErrorCode.UNAUTHENTICATED) + .put("ALREADY_EXISTS", RemoteConfigErrorCode.ALREADY_EXISTS) + .put("VALIDATION_ERROR", RemoteConfigErrorCode.VALIDATION_ERROR) + .put("VERSION_MISMATCH", RemoteConfigErrorCode.VERSION_MISMATCH) .build(); private static final Pattern RC_ERROR_CODE_PATTERN = Pattern.compile("^\\[(\\w+)\\]:.*$"); diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java index 9a209a274..b89abf1eb 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java @@ -372,4 +372,39 @@ public UserResponse setImageUrl(String imageUrl) { return this; } } + + /** + * The Data Transfer Object for parsing Remote Config versions list responses from the + * Remote Config service. + **/ + public static final class ListVersionsResponse { + @Key("versions") + private List versions; + + @Key("nextPageToken") + private String nextPageToken; + + public List getVersions() { + return versions; + } + + public boolean hasVersions() { + return versions != null && !versions.isEmpty(); + } + + public String getNextPageToken() { + return nextPageToken; + } + + public ListVersionsResponse setNextPageToken(String nextPageToken) { + this.nextPageToken = nextPageToken; + return this; + } + + public ListVersionsResponse setVersions( + List versions) { + this.versions = versions; + return this; + } + } } diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index 5bf7c8da6..690abcb99 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -17,6 +17,7 @@ package com.google.firebase.remoteconfig; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -49,6 +50,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.net.URLDecoder; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -71,6 +73,9 @@ public class FirebaseRemoteConfigClientImplTest { private static final String MOCK_TEMPLATE_RESPONSE = TestUtils .loadResource("getRemoteConfig.json"); + private static final String MOCK_LIST_VERSIONS_RESPONSE = TestUtils + .loadResource("listRemoteConfigVersions.json"); + private static final String TEST_ETAG = "etag-123456789012-1"; private static final Map EXPECTED_PARAMETERS = ImmutableMap.of( @@ -914,6 +919,191 @@ public void testRollbackErrorWithRcError() throws IOException { } } + // Test listVersions + + @Test + public void testListVersionsWithNullOptions() throws Exception { + response.setContent(MOCK_LIST_VERSIONS_RESPONSE); + + TemplateResponse.ListVersionsResponse versionsList = client.listVersions(null); + + assertTrue(versionsList.hasVersions()); + assertEquals("28", versionsList.getNextPageToken()); + assertEquals(4, versionsList.getVersions().size()); + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + + @Test + public void testListVersionsWithOptions() throws Exception { + response.setContent(MOCK_LIST_VERSIONS_RESPONSE); + + TemplateResponse.ListVersionsResponse versionsList = client.listVersions( + ListVersionsOptions.builder() + .setPageSize(10) + .setPageToken("token") + .setStartTimeMillis(1605219122000L) + .setEndTimeMillis(1606245035000L) + .setEndVersionNumber("29").build()); + + assertTrue(versionsList.hasVersions()); + + HttpRequest request = interceptor.getLastRequest(); + String urlWithoutParameters = request.getUrl().toString() + .substring(0, request.getUrl().toString().lastIndexOf('?')); + final Map expectedQuery = ImmutableMap.of( + "endVersionNumber", "29", + "pageSize", "10", + "pageToken", "token", + "startTime", "2020-11-12T22:12:02.000000000Z", + "endTime", "2020-11-24T19:10:35.000000000Z" + ); + Map actualQuery = new HashMap<>(); + String query = request.getUrl().toURI().getQuery(); + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + actualQuery.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); + } + + assertEquals("GET", request.getRequestMethod()); + assertEquals(TEST_REMOTE_CONFIG_URL + ":listVersions", urlWithoutParameters); + HttpHeaders headers = request.getHeaders(); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals("gzip", headers.getAcceptEncoding()); + assertEquals(expectedQuery, actualQuery); + } + + @Test + public void testListVersionsWithEmptyResponse() throws Exception { + response.setContent("{}"); + + TemplateResponse.ListVersionsResponse versionsList = client.listVersions(null); + + assertFalse(versionsList.hasVersions()); + assertNull(versionsList.getNextPageToken()); + assertNull(versionsList.getVersions()); + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + + @Test + public void testListVersionsHttpError() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("{}"); + + try { + client.listVersions(null); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, + "Unexpected HTTP response with status: " + code + "\n{}", HttpMethods.GET); + } + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + } + + @Test + public void testListVersionsTransportError() { + client = initClientWithFaultyTransport(); + + try { + client.listVersions(null); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertEquals("Unknown error while making a remote service call: transport error", + error.getMessage()); + assertTrue(error.getCause() instanceof IOException); + assertNull(error.getHttpResponse()); + assertNull(error.getRemoteConfigErrorCode()); + } + } + + @Test + public void testListVersionsSuccessResponseWithUnexpectedPayload() { + response.setContent("not valid json"); + + try { + client.listVersions(null); + fail("No error thrown for malformed response"); + } catch (FirebaseRemoteConfigException error) { + assertEquals(ErrorCode.UNKNOWN, error.getErrorCode()); + assertTrue(error.getMessage().startsWith("Error while parsing HTTP response: ")); + assertNotNull(error.getCause()); + assertNotNull(error.getHttpResponse()); + assertNull(error.getRemoteConfigErrorCode()); + } + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + + @Test + public void testListVersionsErrorWithZeroContentResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setZeroContent(); + + try { + client.listVersions(null); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnull", HttpMethods.GET); + } + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + } + + @Test + public void testListVersionsErrorWithMalformedResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("not json"); + + try { + client.listVersions(null); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, HTTP_STATUS_TO_ERROR_CODE.get(code), null, + "Unexpected HTTP response with status: " + code + "\nnot json", HttpMethods.GET); + } + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + } + + @Test + public void testListVersionsErrorWithDetails() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + client.listVersions(null); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", + HttpMethods.GET); + } + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + } + + @Test + public void testListVersionsErrorWithRcError() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + try { + client.listVersions(null); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); + } + checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); + } + } + // App related tests @Test(expected = IllegalArgumentException.class) diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index cd3bfd85d..d3e7fbff2 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -28,6 +28,8 @@ import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.remoteconfig.internal.TemplateResponse; + import java.util.concurrent.ExecutionException; import org.junit.After; @@ -495,6 +497,102 @@ public void testRollbackAsyncWithLongValueFailure() throws InterruptedException } } + // List versions tests + + @Test + public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException { + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ListVersionsPage listVersionsPage = remoteConfig.listVersions(); + + assertEquals("token", listVersionsPage.getNextPageToken()); + } + + @Test + public void testListVersionsWithNoOptionsFailure() { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.listVersions(); + } catch (FirebaseRemoteConfigException e) { + assertSame(TEST_EXCEPTION, e); + } + } + + @Test + public void testListVersionsAsyncWithNoOptions() throws Exception { + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync().get(); + + assertEquals("token", listVersionsPage.getNextPageToken()); + } + + @Test + public void testListVersionsAsyncWithNoOptionsFailure() throws InterruptedException { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.listVersionsAsync().get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + } + + @Test + public void testListVersionsWithOptions() throws FirebaseRemoteConfigException { + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ListVersionsPage listVersionsPage = remoteConfig.listVersions( + ListVersionsOptions.builder().build()); + + assertEquals("token", listVersionsPage.getNextPageToken()); + } + + @Test + public void testListVersionsWithOptionsFailure() { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.listVersions(ListVersionsOptions.builder().build()); + } catch (FirebaseRemoteConfigException e) { + assertSame(TEST_EXCEPTION, e); + } + } + + @Test + public void testListVersionsAsyncWithOptions() throws Exception { + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync( + ListVersionsOptions.builder().build()).get(); + + assertEquals("token", listVersionsPage.getNextPageToken()); + } + + @Test + public void testListVersionsAsyncWithOptionsFailure() throws InterruptedException { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + } + private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); return new FirebaseRemoteConfig(app, client); diff --git a/src/test/java/com/google/firebase/remoteconfig/ListVersionsPageTest.java b/src/test/java/com/google/firebase/remoteconfig/ListVersionsPageTest.java new file mode 100644 index 000000000..a04c7ab4f --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ListVersionsPageTest.java @@ -0,0 +1,338 @@ +/* + * Copyright 2020 Google LLC + * + * 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.google.firebase.remoteconfig; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.remoteconfig.internal.TemplateResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.Test; + +public class ListVersionsPageTest { + + @Test + public void testSinglePage() throws FirebaseRemoteConfigException { + TestVersionSource source = new TestVersionSource(3); + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + assertFalse(page.hasNextPage()); + assertEquals(ListVersionsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + + ImmutableList versions = ImmutableList.copyOf(page.getValues()); + assertEquals(3, versions.size()); + for (int i = 0; i < 3; i++) { + assertEquals("1" + i, versions.get(i).getVersionNumber()); + } + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testMultiplePages() throws FirebaseRemoteConfigException { + ListVersionsPage.VersionsResultBatch result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of( + newVersion("10"), + newVersion("11"), + newVersion("12")), "token"); + TestVersionSource source = new TestVersionSource(result); + ListVersionsPage page1 = new ListVersionsPage.Factory(source).create(); + + assertTrue(page1.hasNextPage()); + assertEquals("token", page1.getNextPageToken()); + ImmutableList versions = ImmutableList.copyOf(page1.getValues()); + assertEquals(3, versions.size()); + for (int i = 0; i < 3; i++) { + assertEquals("1" + i, versions.get(i).getVersionNumber()); + } + + result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of( + newVersion("13"), + newVersion("14"), + newVersion("15")), ListVersionsPage.END_OF_LIST); + source.result = result; + ListVersionsPage page2 = page1.getNextPage(); + + assertNotNull(page2); + assertFalse(page2.hasNextPage()); + assertEquals(ListVersionsPage.END_OF_LIST, page2.getNextPageToken()); + versions = ImmutableList.copyOf(page2.getValues()); + assertEquals(3, versions.size()); + for (int i = 3; i < 6; i++) { + assertEquals("1" + i, versions.get(i - 3).getVersionNumber()); + } + + assertEquals(2, source.calls.size()); + assertNull(source.calls.get(0)); + assertEquals("token", source.calls.get(1).getPageToken()); + + // Should iterate all versions from both pages + int iterations = 0; + for (Version ignored : page1.iterateAll()) { + iterations++; + } + assertEquals(6, iterations); + assertEquals(3, source.calls.size()); + assertEquals("token", source.calls.get(2).getPageToken()); + + // Should only iterate versions in the last page + iterations = 0; + for (Version ignored : page2.iterateAll()) { + iterations++; + } + assertEquals(3, iterations); + assertEquals(3, source.calls.size()); + } + + @Test + public void testListVersionsIterable() throws FirebaseRemoteConfigException { + TestVersionSource source = new TestVersionSource(3); + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + Iterable versions = page.iterateAll(); + + int iterations = 0; + for (Version version : versions) { + assertEquals("1" + iterations, version.getVersionNumber()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + // Should result in a new iterator + iterations = 0; + for (Version version : versions) { + assertEquals("1" + iterations, version.getVersionNumber()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + } + + @Test + public void testListVersionsIterator() throws FirebaseRemoteConfigException { + TestVersionSource source = new TestVersionSource(3); + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + Iterable versions = page.iterateAll(); + Iterator iterator = versions.iterator(); + int iterations = 0; + while (iterator.hasNext()) { + assertEquals("1" + iterations, iterator.next().getVersionNumber()); + iterations++; + } + assertEquals(3, iterations); + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + + while (iterator.hasNext()) { + fail("Should not be able to to iterate any more"); + } + try { + iterator.next(); + fail("Should not be able to iterate any more"); + } catch (NoSuchElementException expected) { + // expected + } + assertEquals(1, source.calls.size()); + } + + @Test + public void testListVersionsPagedIterable() throws FirebaseRemoteConfigException { + ListVersionsPage.VersionsResultBatch result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of( + newVersion("10"), + newVersion("11"), + newVersion("12")), "token"); + TestVersionSource source = new TestVersionSource(result); + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + int iterations = 0; + for (Version version : page.iterateAll()) { + assertEquals("1" + iterations, version.getVersionNumber()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of( + newVersion("13"), + newVersion("14"), + newVersion("15")), ListVersionsPage.END_OF_LIST); + source.result = result; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1).getPageToken()); + } + + @Test + public void testListVersionsPagedIterator() throws FirebaseRemoteConfigException { + ListVersionsPage.VersionsResultBatch result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of( + newVersion("10"), + newVersion("11"), + newVersion("12")), "token"); + TestVersionSource source = new TestVersionSource(result); + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + Iterator versions = page.iterateAll().iterator(); + int iterations = 0; + while (versions.hasNext()) { + assertEquals("1" + iterations, versions.next().getVersionNumber()); + iterations++; + if (iterations == 3) { + assertEquals(1, source.calls.size()); + assertNull(source.calls.get(0)); + result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of( + newVersion("13"), + newVersion("14"), + newVersion("15")), ListVersionsPage.END_OF_LIST); + source.result = result; + } + } + + assertEquals(6, iterations); + assertEquals(2, source.calls.size()); + assertEquals("token", source.calls.get(1).getPageToken()); + assertFalse(versions.hasNext()); + try { + versions.next(); + } catch (NoSuchElementException e) { + // expected + } + } + + @Test + public void testPageWithNoVersions() throws FirebaseRemoteConfigException { + ListVersionsPage.VersionsResultBatch result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of(), + ListVersionsPage.END_OF_LIST); + TestVersionSource source = new TestVersionSource(result); + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + + assertFalse(page.hasNextPage()); + assertEquals(ListVersionsPage.END_OF_LIST, page.getNextPageToken()); + assertNull(page.getNextPage()); + assertEquals(0, ImmutableList.copyOf(page.getValues()).size()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testIterableWithNoVersions() throws FirebaseRemoteConfigException { + ListVersionsPage.VersionsResultBatch result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of(), + ListVersionsPage.END_OF_LIST); + TestVersionSource source = new TestVersionSource(result); + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + for (Version version : page.iterateAll()) { + fail("Should not be able to iterate, but got: " + version); + } + + assertEquals(1, source.calls.size()); + } + + @Test + public void testIteratorWithNoVersions() throws FirebaseRemoteConfigException { + ListVersionsPage.VersionsResultBatch result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of(), + ListVersionsPage.END_OF_LIST); + TestVersionSource source = new TestVersionSource(result); + + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + + assertFalse(iterator.hasNext()); + assertEquals(1, source.calls.size()); + } + + @Test + public void testRemove() throws FirebaseRemoteConfigException, IOException { + ListVersionsPage.VersionsResultBatch result = new ListVersionsPage.VersionsResultBatch( + ImmutableList.of(newVersion("10")), + ListVersionsPage.END_OF_LIST); + TestVersionSource source = new TestVersionSource(result); + + ListVersionsPage page = new ListVersionsPage.Factory(source).create(); + Iterator iterator = page.iterateAll().iterator(); + while (iterator.hasNext()) { + assertNotNull(iterator.next()); + try { + iterator.remove(); + } catch (UnsupportedOperationException expected) { + // expected + } + } + } + + @Test(expected = NullPointerException.class) + public void testNullSource() { + new ListVersionsPage.Factory(null); + } + + private static Version newVersion(String versionNumber) { + TemplateResponse.VersionResponse versionResponse = new TemplateResponse.VersionResponse() + .setVersionNumber(versionNumber) + .setUpdateTime("2020-11-15T06:57:26.342763941Z") + .setUpdateOrigin("ADMIN_SDK") + .setUpdateType("INCREMENTAL_UPDATE") + .setUpdateUser(new TemplateResponse.UserResponse() + .setEmail("firebase-user@account.com") + .setName("dev-admin")) + .setDescription("test version: " + versionNumber); + return new Version(versionResponse); + } + + private static class TestVersionSource implements ListVersionsPage.VersionSource { + + private ListVersionsPage.VersionsResultBatch result; + private List calls = new ArrayList<>(); + + TestVersionSource(int versionCount) { + ImmutableList.Builder versions = ImmutableList.builder(); + for (int i = 0; i < versionCount; i++) { + versions.add(newVersion("1" + i)); + } + this.result = new ListVersionsPage.VersionsResultBatch(versions.build(), + ListVersionsPage.END_OF_LIST); + } + + TestVersionSource(ListVersionsPage.VersionsResultBatch result) { + this.result = result; + } + + @Override + public ListVersionsPage.VersionsResultBatch fetch( + ListVersionsOptions listVersionsOptions) { + calls.add(listVersionsOptions); + return result; + } + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java index 3d0fd5982..9ca58508d 100644 --- a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java +++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java @@ -16,23 +16,33 @@ package com.google.firebase.remoteconfig; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ListVersionsResponse; + public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{ - private Template resultTemplate; - private FirebaseRemoteConfigException exception; + private final Template resultTemplate; + private final FirebaseRemoteConfigException exception; + private final ListVersionsResponse listVersionsResponse; private MockRemoteConfigClient(Template resultTemplate, + ListVersionsResponse listVersionsResponse, FirebaseRemoteConfigException exception) { this.resultTemplate = resultTemplate; + this.listVersionsResponse = listVersionsResponse; this.exception = exception; } static MockRemoteConfigClient fromTemplate(Template resultTemplate) { - return new MockRemoteConfigClient(resultTemplate, null); + return new MockRemoteConfigClient(resultTemplate, null, null); + } + + static MockRemoteConfigClient fromListVersionsResponse( + ListVersionsResponse listVersionsResponse) { + return new MockRemoteConfigClient(null, listVersionsResponse, null); } static MockRemoteConfigClient fromException(FirebaseRemoteConfigException exception) { - return new MockRemoteConfigClient(null, exception); + return new MockRemoteConfigClient(null, null, exception); } @Override @@ -67,4 +77,13 @@ public Template rollback(String versionNumber) throws FirebaseRemoteConfigExcept } return resultTemplate; } + + @Override + public ListVersionsResponse listVersions( + ListVersionsOptions options) throws FirebaseRemoteConfigException { + if (exception != null) { + throw exception; + } + return listVersionsResponse; + } } diff --git a/src/test/resources/listRemoteConfigVersions.json b/src/test/resources/listRemoteConfigVersions.json new file mode 100644 index 000000000..2823db3ef --- /dev/null +++ b/src/test/resources/listRemoteConfigVersions.json @@ -0,0 +1,50 @@ +{ + "versions": [ + { + "versionNumber": "32", + "updateTime": "2020-11-24T19:10:35.506793Z", + "updateUser": { + "email": "admin@email.com", + "imageUrl": "https://photo.jpg" + }, + "description": "Rollback to version 29", + "updateOrigin": "REST_API", + "updateType": "ROLLBACK", + "rollbackSource": "29" + }, + { + "versionNumber": "31", + "updateTime": "2020-11-24T19:09:40.249437Z", + "updateUser": { + "email": "admin@email.com", + "imageUrl": "https://photo.jpg" + }, + "description": "new template", + "updateOrigin": "REST_API", + "updateType": "INCREMENTAL_UPDATE" + }, + { + "versionNumber": "30", + "updateTime": "2020-11-24T19:08:30.613942Z", + "updateUser": { + "email": "admin@email.com", + "imageUrl": "https://photo.jpg" + }, + "description": "new template", + "updateOrigin": "REST_API", + "updateType": "INCREMENTAL_UPDATE" + }, + { + "versionNumber": "29", + "updateTime": "2020-11-12T22:12:02.180181Z", + "updateUser": { + "email": "admin@email.com", + "imageUrl": "https://photo.jpg" + }, + "description": "new template", + "updateOrigin": "REST_API", + "updateType": "INCREMENTAL_UPDATE" + } + ], + "nextPageToken": "28" +}