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"
+}