From 5eaa9ef411acb17462f278c21c62093e2691c0bc Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Mon, 23 Jun 2025 21:09:43 +0530 Subject: [PATCH 01/29] Implementation for Fetching and Caching Server Side Remote Config (#1107)009 * Implementation for Fetching and Caching Server Side Remote Config * implementation of fetch , cache and load of template --------- Co-authored-by: Varun Rathore --- .../firebase/remoteconfig/AndCondition.java | 64 ++++ .../remoteconfig/FirebaseRemoteConfig.java | 68 ++++ .../FirebaseRemoteConfigClient.java | 3 + .../FirebaseRemoteConfigClientImpl.java | 24 ++ .../firebase/remoteconfig/KeysAndValues.java | 136 ++++++++ .../firebase/remoteconfig/OneOfCondition.java | 102 ++++++ .../firebase/remoteconfig/OrCondition.java | 61 ++++ .../remoteconfig/ServerCondition.java | 90 +++++ .../firebase/remoteconfig/ServerConfig.java | 103 ++++++ .../firebase/remoteconfig/ServerTemplate.java | 37 ++ .../remoteconfig/ServerTemplateData.java | 216 ++++++++++++ .../remoteconfig/ServerTemplateImpl.java | 95 ++++++ .../google/firebase/remoteconfig/Value.java | 136 ++++++++ .../firebase/remoteconfig/ValueSource.java | 31 ++ .../internal/ServerTemplateResponse.java | 321 ++++++++++++++++++ .../FirebaseRemoteConfigClientImplTest.java | 2 + .../FirebaseRemoteConfigTest.java | 74 ++++ .../remoteconfig/MockRemoteConfigClient.java | 22 +- 18 files changed, 1582 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/google/firebase/remoteconfig/AndCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/OrCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerConfig.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/Value.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/ValueSource.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java diff --git a/src/main/java/com/google/firebase/remoteconfig/AndCondition.java b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java new file mode 100644 index 000000000..3be10560a --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java @@ -0,0 +1,64 @@ + +/* + * Copyright 2025 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.AndConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +final class AndCondition { + private final ImmutableList conditions; + + AndCondition(@NonNull List conditions) { + checkNotNull(conditions, "List of conditions for AND operation must not be null."); + checkArgument(!conditions.isEmpty(), + "List of conditions for AND operation must not be empty."); + this.conditions = ImmutableList.copyOf(conditions); + } + + AndCondition(AndConditionResponse andConditionResponse) { + List conditionList = andConditionResponse.getConditions(); + checkNotNull(conditionList, "List of conditions for AND operation must not be null."); + checkArgument(!conditionList.isEmpty(), + "List of conditions for AND operation must not be empty"); + this.conditions = conditionList.stream() + .map(OneOfCondition::new) + .collect(ImmutableList.toImmutableList()); + } + + @NonNull + List getConditions() { + return new ArrayList<>(conditions); + } + + AndConditionResponse toAndConditionResponse() { + return new AndConditionResponse() + .setConditions(this.conditions.stream() + .map(OneOfCondition::toOneOfConditionResponse) + .collect(Collectors.toList())); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index 41a0afbe4..e3edce4e4 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -101,6 +101,73 @@ protected Template execute() throws FirebaseRemoteConfigException { }; } + /** + * Alternative to {@link #getServerTemplate} where developers can initialize with a pre-cached + * template or config. + */ + public ServerTemplateImpl.Builder serverTemplateBuilder() { + return new ServerTemplateImpl.Builder(this.remoteConfigClient); + } + + /** + * Initializes a template instance and loads the latest template data. + * + * @param defaultConfig Default parameter values to use if a getter references a parameter not + * found in the template. + * @return A {@link Template} instance with the latest template data. + */ + public ServerTemplate getServerTemplate(KeysAndValues defaultConfig) + throws FirebaseRemoteConfigException { + return getServerTemplateOp(defaultConfig).call(); + } + + /** + * Initializes a template instance without any defaults and loads the latest template data. + * + * @return A {@link Template} instance with the latest template data. + */ + public ServerTemplate getServerTemplate() throws FirebaseRemoteConfigException { + return getServerTemplate(null); + } + + /** + * Initializes a template instance and asynchronously loads the latest template data. + * + * @param defaultConfig Default parameter values to use if a getter references a parameter not + * found in the template. + * @return A {@link Template} instance with the latest template data. + */ + public ApiFuture getServerTemplateAsync(KeysAndValues defaultConfig) { + return getServerTemplateOp(defaultConfig).callAsync(app); + } + + /** + * Initializes a template instance without any defaults and asynchronously loads the latest + * template data. + * + * @return A {@link Template} instance with the latest template data. + */ + public ApiFuture getServerTemplateAsync() { + return getServerTemplateAsync(null); + } + + private CallableOperation getServerTemplateOp( + KeysAndValues defaultConfig) { + return new CallableOperation() { + @Override + protected ServerTemplate execute() throws FirebaseRemoteConfigException { + String serverTemplateData = remoteConfigClient.getServerTemplate(); + ServerTemplate template = + serverTemplateBuilder() + .defaultConfig(defaultConfig) + .cachedTemplate(serverTemplateData) + .build(); + + return template; + } + }; + } + /** * Gets the requested version of the of the Remote Config template. * @@ -413,3 +480,4 @@ public void destroy() { } } } + diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java index 9fdb596d6..2143d07d1 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java @@ -40,4 +40,7 @@ Template publishTemplate(Template template, boolean validateOnly, ListVersionsResponse listVersions( ListVersionsOptions options) throws FirebaseRemoteConfigException; + + String getServerTemplate() 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 7425673fb..d84abae84 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java @@ -38,6 +38,7 @@ import com.google.firebase.internal.NonNull; import com.google.firebase.internal.SdkUtils; import com.google.firebase.remoteconfig.internal.RemoteConfigServiceErrorResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse; import java.io.IOException; @@ -51,6 +52,9 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient private static final String REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/remoteConfig"; + private static final String SERVER_REMOTE_CONFIG_URL = + "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/namespaces/firebase-server/serverRemoteConfig"; + private static final Map COMMON_HEADERS = ImmutableMap.of( "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion(), @@ -62,6 +66,7 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient ); private final String remoteConfigUrl; + private final String serverRemoteConfigUrl; private final HttpRequestFactory requestFactory; private final JsonFactory jsonFactory; private final ErrorHandlingHttpClient httpClient; @@ -69,6 +74,7 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient private FirebaseRemoteConfigClientImpl(Builder builder) { checkArgument(!Strings.isNullOrEmpty(builder.projectId)); this.remoteConfigUrl = String.format(REMOTE_CONFIG_URL, builder.projectId); + this.serverRemoteConfigUrl = String.format(SERVER_REMOTE_CONFIG_URL, builder.projectId); this.requestFactory = checkNotNull(builder.requestFactory); this.jsonFactory = checkNotNull(builder.jsonFactory); HttpResponseInterceptor responseInterceptor = builder.responseInterceptor; @@ -82,6 +88,11 @@ String getRemoteConfigUrl() { return remoteConfigUrl; } + @VisibleForTesting + String getServerRemoteConfigUrl() { + return serverRemoteConfigUrl; + } + @VisibleForTesting HttpRequestFactory getRequestFactory() { return requestFactory; @@ -102,6 +113,18 @@ public Template getTemplate() throws FirebaseRemoteConfigException { return template.setETag(getETag(response)); } + @Override + public String getServerTemplate() throws FirebaseRemoteConfigException { + HttpRequestInfo request = + HttpRequestInfo.buildGetRequest(serverRemoteConfigUrl).addAllHeaders(COMMON_HEADERS); + IncomingHttpResponse response = httpClient.send(request); + ServerTemplateResponse templateResponse = httpClient.parse(response, + ServerTemplateResponse.class); + ServerTemplateData serverTemplateData = new ServerTemplateData(templateResponse); + serverTemplateData.setETag(getETag(response)); + return serverTemplateData.toJSON(); + } + @Override public Template getTemplateAtVersion( @NonNull String versionNumber) throws FirebaseRemoteConfigException { @@ -267,3 +290,4 @@ private RemoteConfigServiceErrorResponse safeParse(String response) { } } } + diff --git a/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java new file mode 100644 index 000000000..97064d96a --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java @@ -0,0 +1,136 @@ + +/* + * Copyright 2025 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 com.google.common.collect.ImmutableMap; +import com.google.firebase.internal.NonNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents data stored in context passed to server-side Remote Config. + */ +public class KeysAndValues { + final ImmutableMap keysAndValues; + + private KeysAndValues(@NonNull Builder builder) { + keysAndValues = ImmutableMap.builder().putAll(builder.keysAndValues).build(); + } + + /** + * Checks whether a key is present in the context. + * + * @param key The key for data stored in context. + * @return Boolean representing whether the key passed is present in context. + */ + public boolean containsKey(String key) { + return keysAndValues.containsKey(key); + } + + /** + * Gets the value of the data stored in context. + * + * @param key The key for data stored in context. + * @return Value assigned to the key in context. + */ + public String get(String key) { + return keysAndValues.get(key); + } + + /** + * Builder class for KeysAndValues using which values will be assigned to + * private variables. + */ + public static class Builder { + // Holds the converted pairs of custom keys and values. + private final Map keysAndValues = new HashMap<>(); + + /** + * Adds a context data with string value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, @NonNull String value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + checkArgument(!Strings.isNullOrEmpty(value), "Context key must not be null or empty."); + keysAndValues.put(key, value); + return this; + } + + /** + * Adds a context data with boolean value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, boolean value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + keysAndValues.put(key, Boolean.toString(value)); + return this; + } + + /** + * Adds a context data with double value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, double value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + keysAndValues.put(key, Double.toString(value)); + return this; + } + + /** + * Adds a context data with long value. + * + * @param key Identifies the value in context. + * @param value Value assigned to the context. + * @return Reference to class itself so that more data can be added. + */ + @NonNull + public Builder put(@NonNull String key, long value) { + checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty."); + keysAndValues.put(key, Long.toString(value)); + return this; + } + + /** + * Creates an instance of KeysAndValues with the values assigned through + * builder. + * + * @return instance of KeysAndValues + */ + @NonNull + public KeysAndValues build() { + return new KeysAndValues(this); + } + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java new file mode 100644 index 000000000..255cee565 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025 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.common.annotations.VisibleForTesting; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; + +class OneOfCondition { + private OrCondition orCondition; + private AndCondition andCondition; + private String trueValue; + private String falseValue; + + OneOfCondition(OneOfConditionResponse oneOfconditionResponse) { + if (oneOfconditionResponse.getOrCondition() != null) { + this.orCondition = new OrCondition(oneOfconditionResponse.getOrCondition()); + } + if (oneOfconditionResponse.getAndCondition() != null) { + this.andCondition = new AndCondition(oneOfconditionResponse.getAndCondition()); + } + } + + @VisibleForTesting + OneOfCondition() { + this.orCondition = null; + this.andCondition = null; + this.trueValue = null; + this.falseValue = null; + } + + @Nullable + OrCondition getOrCondition() { + return orCondition; + } + + @Nullable + AndCondition getAndCondition() { + return andCondition; + } + + @Nullable + String isTrue() { + return trueValue; + } + + @Nullable + String isFalse() { + return falseValue; + } + + OneOfCondition setOrCondition(@NonNull OrCondition orCondition) { + checkNotNull(orCondition, "`Or` condition cannot be set to null."); + this.orCondition = orCondition; + return this; + } + + OneOfCondition setAndCondition(@NonNull AndCondition andCondition) { + checkNotNull(andCondition, "`And` condition cannot be set to null."); + this.andCondition = andCondition; + return this; + } + + OneOfCondition setTrue() { + this.trueValue = "true"; + return this; + } + + OneOfCondition setFalse() { + this.falseValue = "false"; + return this; + } + + OneOfConditionResponse toOneOfConditionResponse() { + OneOfConditionResponse oneOfConditionResponse = new OneOfConditionResponse(); + if (this.andCondition != null) { + oneOfConditionResponse.setAndCondition(this.andCondition.toAndConditionResponse()); + } + if (this.orCondition != null) { + oneOfConditionResponse.setOrCondition(this.orCondition.toOrConditionResponse()); + } + return oneOfConditionResponse; + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/OrCondition.java b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java new file mode 100644 index 000000000..4d258f23b --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java @@ -0,0 +1,61 @@ + +/* + * Copyright 2025 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OrConditionResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +final class OrCondition { + private final ImmutableList conditions; + + public OrCondition(@NonNull List conditions) { + checkNotNull(conditions, "List of conditions for OR operation must not be null."); + checkArgument(!conditions.isEmpty(), "List of conditions for OR operation must not be empty."); + this.conditions = ImmutableList.copyOf(conditions); + } + + OrCondition(OrConditionResponse orConditionResponse) { + List conditionList = orConditionResponse.getConditions(); + checkNotNull(conditionList, "List of conditions for AND operation cannot be null."); + checkArgument(!conditionList.isEmpty(), "List of conditions for AND operation cannot be empty"); + this.conditions = conditionList.stream() + .map(OneOfCondition::new) + .collect(ImmutableList.toImmutableList()); + } + + @NonNull + List getConditions() { + return new ArrayList<>(conditions); + } + + OrConditionResponse toOrConditionResponse() { + return new OrConditionResponse() + .setConditions(this.conditions.stream() + .map(OneOfCondition::toOneOfConditionResponse) + .collect(Collectors.toList())); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java b/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java new file mode 100644 index 000000000..f16aeffc8 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.ServerConditionResponse; + +import java.util.Objects; + +final class ServerCondition { + + private String name; + private OneOfCondition serverCondition; + + ServerCondition(@NonNull String name, @NonNull OneOfCondition condition) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + this.serverCondition = condition; + } + + ServerCondition(@NonNull ServerConditionResponse serverConditionResponse) { + checkNotNull(serverConditionResponse); + this.name = serverConditionResponse.getName(); + this.serverCondition = new OneOfCondition(serverConditionResponse.getServerCondition()); + } + + @NonNull + String getName() { + return name; + } + + @NonNull + OneOfCondition getCondition() { + return serverCondition; + } + + ServerCondition setName(@NonNull String name) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + return this; + } + + ServerCondition setServerCondition(@NonNull OneOfCondition condition) { + checkNotNull(condition, "condition must not be null or empty"); + this.serverCondition = condition; + return this; + } + + ServerConditionResponse toServerConditionResponse() { + return new ServerConditionResponse().setName(this.name) + .setServerCondition(this.serverCondition.toOneOfConditionResponse()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerCondition condition = (ServerCondition) o; + return Objects.equals(name, condition.name) + && Objects.equals(serverCondition, condition.serverCondition); + } + + @Override + public int hashCode() { + return Objects.hash(name, serverCondition); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java new file mode 100644 index 000000000..9e063009f --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java @@ -0,0 +1,103 @@ +/* + * Copyright 2025 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 com.google.firebase.internal.NonNull; + +import java.util.Map; + +/** + * Represents the configuration produced by evaluating a server template. + */ +public final class ServerConfig { + private final Map configValues; + + ServerConfig(Map configValues) { + this.configValues = configValues; + } + + /** + * Gets the value for the given key as a string. Convenience method for calling + * serverConfig.getValue(key).asString(). + * + * @param key The name of the parameter. + * @return config value for the given key as string. + */ + @NonNull + public String getString(@NonNull String key) { + return this.getValue(key).asString(); + } + + /** + * Gets the value for the given key as a boolean.Convenience method for calling + * serverConfig.getValue(key).asBoolean(). + * + * @param key The name of the parameter. + * @return config value for the given key as boolean. + */ + @NonNull + public boolean getBoolean(@NonNull String key) { + return this.getValue(key).asBoolean(); + } + + /** + * Gets the value for the given key as long.Convenience method for calling + * serverConfig.getValue(key).asLong(). + * + * @param key The name of the parameter. + * @return config value for the given key as long. + */ + @NonNull + public long getLong(@NonNull String key) { + return this.getValue(key).asLong(); + } + + /** + * Gets the value for the given key as double.Convenience method for calling + * serverConfig.getValue(key).asDouble(). + * + * @param key The name of the parameter. + * @return config value for the given key as double. + */ + @NonNull + public double getDouble(@NonNull String key) { + return this.getValue(key).asDouble(); + } + + /** + * Gets the {@link ValueSource} for the given key. + * + * @param key The name of the parameter. + * @return config value source for the given key. + */ + @NonNull + public ValueSource getValueSource(@NonNull String key) { + return this.getValue(key).getSource(); + } + + private Value getValue(String key) { + checkArgument(!Strings.isNullOrEmpty(key), "Server config key cannot be null or empty."); + if (configValues.containsKey(key)) { + return configValues.get(key); + } + return new Value(ValueSource.STATIC); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java new file mode 100644 index 000000000..f16992744 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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 com.google.api.core.ApiFuture; + +public interface ServerTemplate { + public interface Builder { + + Builder defaultConfig(KeysAndValues config); + + Builder cachedTemplate(String templateJson); + + ServerTemplate build(); + } + /** + * Fetches and caches the current active version of the project. + */ + ApiFuture load() throws FirebaseRemoteConfigException; + + String toJson(); +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java new file mode 100644 index 000000000..34674855f --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java @@ -0,0 +1,216 @@ + +/* + * Copyright 2025 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.json.JsonFactory; +import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class ServerTemplateData { + + private String etag; + private Map parameters; + private List serverConditions; + private Map parameterGroups; + private Version version; + + + ServerTemplateData(String etag) { + this.parameters = new HashMap<>(); + this.serverConditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); + this.etag = etag; + } + + ServerTemplateData() { + this((String) null); + } + + ServerTemplateData(@NonNull ServerTemplateResponse serverTemplateResponse) { + checkNotNull(serverTemplateResponse); + this.parameters = new HashMap<>(); + this.serverConditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); + if (serverTemplateResponse.getParameters() != null) { + for (Map.Entry entry : + serverTemplateResponse.getParameters().entrySet()) { + this.parameters.put(entry.getKey(), new Parameter(entry.getValue())); + } + } + if (serverTemplateResponse.getServerConditions() != null) { + for (ServerTemplateResponse.ServerConditionResponse conditionResponse : + serverTemplateResponse.getServerConditions()) { + this.serverConditions.add(new ServerCondition(conditionResponse)); + } + } + if (serverTemplateResponse.getParameterGroups() != null) { + for (Map.Entry entry : + serverTemplateResponse.getParameterGroups().entrySet()) { + this.parameterGroups.put(entry.getKey(), new ParameterGroup(entry.getValue())); + } + } + if (serverTemplateResponse.getVersion() != null) { + this.version = new Version(serverTemplateResponse.getVersion()); + } + this.etag = serverTemplateResponse.getEtag(); + } + + + static ServerTemplateData fromJSON(@NonNull String json) + throws FirebaseRemoteConfigException { + checkArgument(!Strings.isNullOrEmpty(json), "JSON String must not be null or empty."); + // using the default json factory as no rpc calls are made here + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + try { + ServerTemplateResponse serverTemplateResponse = + jsonFactory.createJsonParser(json).parseAndClose(ServerTemplateResponse.class); + return new ServerTemplateData(serverTemplateResponse); + } catch (IOException e) { + throw new FirebaseRemoteConfigException( + ErrorCode.INVALID_ARGUMENT, "Unable to parse JSON string."); + } + } + + + String getETag() { + return this.etag; + } + + + @NonNull + public Map getParameters() { + return this.parameters; + } + + @NonNull + List getServerConditions() { + return serverConditions; + } + + @NonNull + Map getParameterGroups() { + return parameterGroups; + } + + Version getVersion() { + return version; + } + + ServerTemplateData setParameters(@NonNull Map parameters) { + checkNotNull(parameters, "parameters must not be null."); + this.parameters = parameters; + return this; + } + + + ServerTemplateData setServerConditions(@NonNull List conditions) { + checkNotNull(conditions, "conditions must not be null."); + this.serverConditions = conditions; + return this; + } + + ServerTemplateData setParameterGroups( + @NonNull Map parameterGroups) { + checkNotNull(parameterGroups, "parameter groups must not be null."); + this.parameterGroups = parameterGroups; + return this; + } + + ServerTemplateData setVersion(Version version) { + this.version = version; + return this; + } + + String toJSON() { + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + try { + return jsonFactory.toString(this.toServerTemplateResponse(true)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + ServerTemplateData setETag(String etag) { + this.etag = etag; + return this; + } + + ServerTemplateResponse toServerTemplateResponse(boolean includeAll) { + Map parameterResponses = new HashMap<>(); + for (Map.Entry entry : this.parameters.entrySet()) { + parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse()); + } + List serverConditionResponses = + new ArrayList<>(); + for (ServerCondition condition : this.serverConditions) { + serverConditionResponses.add(condition.toServerConditionResponse()); + } + Map parameterGroupResponse = new HashMap<>(); + for (Map.Entry entry : this.parameterGroups.entrySet()) { + parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse()); + } + TemplateResponse.VersionResponse versionResponse = + (this.version == null) ? null : this.version.toVersionResponse(includeAll); + ServerTemplateResponse serverTemplateResponse = + new ServerTemplateResponse() + .setParameters(parameterResponses) + .setServerConditions(serverConditionResponses) + .setParameterGroups(parameterGroupResponse) + .setVersion(versionResponse); + if (includeAll) { + return serverTemplateResponse.setEtag(this.etag); + } + return serverTemplateResponse; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerTemplateData template = (ServerTemplateData) o; + return Objects.equals(etag, template.etag) + && Objects.equals(parameters, template.parameters) + && Objects.equals(serverConditions, template.serverConditions) + && Objects.equals(parameterGroups, template.parameterGroups) + && Objects.equals(version, template.version); + } + + @Override + public int hashCode() { + return Objects.hash(etag, parameters, serverConditions, parameterGroups, version); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java new file mode 100644 index 000000000..81c71e898 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -0,0 +1,95 @@ + +/* + * Copyright 2025 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 com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.concurrent.atomic.AtomicReference; + +public final class ServerTemplateImpl implements ServerTemplate { + + private final KeysAndValues defaultConfig; + private FirebaseRemoteConfigClient client; + private ServerTemplateData cache; + private final AtomicReference cachedTemplate; // Added field for cached template + + public static class Builder implements ServerTemplate.Builder { + private KeysAndValues defaultConfig; + private String cachedTemplate; + private FirebaseRemoteConfigClient client; + + Builder(FirebaseRemoteConfigClient remoteConfigClient) { + this.client = remoteConfigClient; + } + + @Override + public Builder defaultConfig(KeysAndValues config) { + this.defaultConfig = config; + return this; + } + + @Override + public Builder cachedTemplate(String templateJson) { + this.cachedTemplate = templateJson; + return this; + } + + @Override + public ServerTemplate build() { + return new ServerTemplateImpl(this); + } + } + + private ServerTemplateImpl(Builder builder) { + this.defaultConfig = builder.defaultConfig; + this.cachedTemplate = new AtomicReference<>(builder.cachedTemplate); + this.client = builder.client; + try { + this.cache = ServerTemplateData.fromJSON(this.cachedTemplate.get()); + } catch (FirebaseRemoteConfigException e) { + e.printStackTrace(); + } + } + + @Override + public ApiFuture load() throws FirebaseRemoteConfigException { + String serverTemplate = client.getServerTemplate(); + this.cachedTemplate.set(serverTemplate); + this.cache = ServerTemplateData.fromJSON(serverTemplate); + return ApiFutures.immediateFuture(null); + } + + // Add getters or other methods as needed + public KeysAndValues getDefaultConfig() { + return defaultConfig; + } + + public String getCachedTemplate() { + return cachedTemplate.get(); + } + + @Override + public String toJson() { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + return gson.toJson(this.cache); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/Value.java b/src/main/java/com/google/firebase/remoteconfig/Value.java new file mode 100644 index 000000000..7f04aefeb --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/Value.java @@ -0,0 +1,136 @@ +/* + * Copyright 2025 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.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps a parameter value with metadata and type-safe getters. Type-safe + * getters insulate application logic from remote changes to parameter names and + * types. + */ +class Value { + private static final Logger logger = LoggerFactory.getLogger(Value.class); + private static final boolean DEFAULT_VALUE_FOR_BOOLEAN = false; + private static final String DEFAULT_VALUE_FOR_STRING = ""; + private static final long DEFAULT_VALUE_FOR_LONG = 0; + private static final double DEFAULT_VALUE_FOR_DOUBLE = 0; + private static final ImmutableList BOOLEAN_TRUTHY_VALUES = ImmutableList.of("1", "true", + "t", "yes", "y", "on"); + + private final ValueSource source; + private final String value; + + /** + * Creates a new {@link Value} object. + * + * @param source Indicates the source of a value. + * @param value Indicates a parameter value. + */ + Value(@NonNull ValueSource source, String value) { + checkNotNull(source, "Value source cannot be null."); + this.source = source; + this.value = value; + } + + /** + * Creates a new {@link Value} object with default value. + * + * @param source Indicates the source of a value. + */ + Value(@NonNull ValueSource source) { + this(source, DEFAULT_VALUE_FOR_STRING); + } + + /** + * Gets the value as a string. + * + * @return value as string + */ + @NonNull + String asString() { + return this.value; + } + + /** + * Gets the value as a boolean.The following values (case + * insensitive) are interpreted as true: "1", "true", "t", "yes", "y", "on". + * Other values are interpreted as false. + * + * @return value as boolean + */ + @NonNull + boolean asBoolean() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_BOOLEAN; + } + return BOOLEAN_TRUTHY_VALUES.contains(value.toLowerCase()); + } + + /** + * Gets the value as long. Comparable to calling Number(value) || 0. + * + * @return value as long + */ + @NonNull + long asLong() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_LONG; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + logger.warn("Unable to convert %s to long type.", value); + return DEFAULT_VALUE_FOR_LONG; + } + } + + /** + * Gets the value as double. Comparable to calling Number(value) || 0. + * + * @return value as double + */ + @NonNull + double asDouble() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_DOUBLE; + } + try { + return Double.parseDouble(this.value); + } catch (NumberFormatException e) { + logger.warn("Unable to convert %s to double type.", value); + return DEFAULT_VALUE_FOR_DOUBLE; + } + } + + /** + * Gets the {@link ValueSource} for the given key. + * + * @return source. + */ + @NonNull + ValueSource getSource() { + return source; + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ValueSource.java b/src/main/java/com/google/firebase/remoteconfig/ValueSource.java new file mode 100644 index 000000000..c870e8514 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ValueSource.java @@ -0,0 +1,31 @@ + +/* + * Copyright 2025 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; + +/** + * Indicates the source of a value. + * "static" indicates the value was defined by a static constant. + * "default" indicates the value was defined by default config. + * "remote" indicates the value was defined by config produced by evaluating a template. + */ +public enum ValueSource { + STATIC, + REMOTE, + DEFAULT +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java new file mode 100644 index 000000000..87f624a78 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java @@ -0,0 +1,321 @@ +/* + * Copyright 2025 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.internal; + +import com.google.api.client.util.Key; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterGroupResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; + +import java.util.List; +import java.util.Map; + +/** + * The Data Transfer Object for parsing Remote Config template responses from the Remote Config + * service. + */ +public final class ServerTemplateResponse { + @Key("parameters") + private Map parameters; + + @Key("conditions") + private List serverConditions; + + @Key("parameterGroups") + private Map parameterGroups; + + @Key("version") + private VersionResponse version; + + // For local JSON serialization and deserialization purposes only. + // ETag in response type is never set by the HTTP response. + @Key("etag") + private String etag; + + public Map getParameters() { + return parameters; + } + + public List getServerConditions() { + return serverConditions; + } + + public Map getParameterGroups() { + return parameterGroups; + } + + public VersionResponse getVersion() { + return version; + } + + public String getEtag() { + return etag; + } + + public ServerTemplateResponse setParameters(Map parameters) { + this.parameters = parameters; + return this; + } + + public ServerTemplateResponse setServerConditions( + List serverConditions) { + this.serverConditions = serverConditions; + return this; + } + + public ServerTemplateResponse setParameterGroups( + Map parameterGroups) { + this.parameterGroups = parameterGroups; + return this; + } + + public ServerTemplateResponse setVersion(VersionResponse version) { + this.version = version; + return this; + } + + public ServerTemplateResponse setEtag(String etag) { + this.etag = etag; + return this; + } + + /** + * The Data Transfer Object for parsing Remote Config condition responses from the Remote Config + * service. + */ + public static final class ServerConditionResponse { + + @Key("name") + private String name; + + @Key("condition") + private OneOfConditionResponse condition; + + public String getName() { + return name; + } + + public OneOfConditionResponse getServerCondition() { + return condition; + } + + public ServerConditionResponse setName(String name) { + this.name = name; + return this; + } + + public ServerConditionResponse setServerCondition(OneOfConditionResponse condition) { + this.condition = condition; + return this; + } + } + + public static final class OneOfConditionResponse { + @Key("orCondition") + private OrConditionResponse orCondition; + + @Key("andCondition") + private AndConditionResponse andCondition; + + @Key("customSignal") + private CustomSignalConditionResponse customSignalCondition; + + @Key("percent") + private PercentConditionResponse percentCondition; + + public OrConditionResponse getOrCondition() { + return orCondition; + } + + public AndConditionResponse getAndCondition() { + return andCondition; + } + + public PercentConditionResponse getPercentCondition() { + return percentCondition; + } + + public CustomSignalConditionResponse getCustomSignalCondition() { + return customSignalCondition; + } + + public OneOfConditionResponse setOrCondition(OrConditionResponse orCondition) { + this.orCondition = orCondition; + return this; + } + + public OneOfConditionResponse setAndCondition(AndConditionResponse andCondition) { + this.andCondition = andCondition; + return this; + } + + public OneOfConditionResponse setCustomSignalCondition( + CustomSignalConditionResponse customSignalCondition) { + this.customSignalCondition = customSignalCondition; + return this; + } + + public OneOfConditionResponse setPercentCondition(PercentConditionResponse percentCondition) { + this.percentCondition = percentCondition; + return this; + } + } + + public static final class OrConditionResponse { + @Key("conditions") + private List conditions; + + public List getConditions() { + return conditions; + } + + public OrConditionResponse setConditions(List conditions) { + this.conditions = conditions; + return this; + } + } + + public static final class AndConditionResponse { + @Key("conditions") + private List conditions; + + public List getConditions() { + return conditions; + } + + public AndConditionResponse setConditions(List conditions) { + this.conditions = conditions; + return this; + } + } + + public static final class CustomSignalConditionResponse { + @Key("customSignalOperator") + private String operator; + + @Key("customSignalKey") + private String key; + + @Key("targetCustomSignalValues") + private List targetValues; + + public String getOperator() { + return operator; + } + + public String getKey() { + return key; + } + + public List getTargetValues() { + return targetValues; + } + + public CustomSignalConditionResponse setOperator(String operator) { + this.operator = operator; + return this; + } + + public CustomSignalConditionResponse setKey(String key) { + this.key = key; + return this; + } + + public CustomSignalConditionResponse setTargetValues(List targetValues) { + this.targetValues = targetValues; + return this; + } + } + + public static final class PercentConditionResponse { + @Key("microPercent") + private int microPercent; + + @Key("microPercentRange") + private MicroPercentRangeResponse microPercentRange; + + @Key("percentOperator") + private String percentOperator; + + @Key("seed") + private String seed; + + public int getMicroPercent() { + return microPercent; + } + + public MicroPercentRangeResponse getMicroPercentRange() { + return microPercentRange; + } + + public String getPercentOperator() { + return percentOperator; + } + + public String getSeed() { + return seed; + } + + public PercentConditionResponse setMicroPercent(int microPercent) { + this.microPercent = microPercent; + return this; + } + + public PercentConditionResponse setMicroPercentRange( + MicroPercentRangeResponse microPercentRange) { + this.microPercentRange = microPercentRange; + return this; + } + + public PercentConditionResponse setPercentOperator(String percentOperator) { + this.percentOperator = percentOperator; + return this; + } + + public PercentConditionResponse setSeed(String seed) { + this.seed = seed; + return this; + } + } + + public static final class MicroPercentRangeResponse { + @Key("microPercentLowerBound") + private int microPercentLowerBound; + + @Key("microPercentUpperBound") + private int microPercentUpperBound; + + public int getMicroPercentLowerBound() { + return microPercentLowerBound; + } + + public int getMicroPercentUpperBound() { + return microPercentUpperBound; + } + + public MicroPercentRangeResponse setMicroPercentLowerBound(int microPercentLowerBound) { + this.microPercentLowerBound = microPercentLowerBound; + return this; + } + + public MicroPercentRangeResponse setMicroPercentUpperBound(int microPercentUpperBound) { + this.microPercentUpperBound = microPercentUpperBound; + 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 edc52a19d..aea2dd1eb 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -42,6 +42,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.OutgoingHttpRequest; + import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.SdkUtils; @@ -1239,3 +1240,4 @@ private void checkExceptionFromHttpResponse( assertTrue(request.getUrl().startsWith("https://firebaseremoteconfig.googleapis.com")); } } + diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index d3e7fbff2..66821abb8 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -35,12 +35,22 @@ import org.junit.After; import org.junit.Test; +/** Unit tests + * for {@link FirebaseRemoteConfig}. + * */ public class FirebaseRemoteConfigTest { private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); + private static final String TEST_SERVER_TEMPLATE = + "{\n" + + " \"etag\": \"etag-123456789012-1\",\n" + + " \"parameters\": {},\n" + + " \"serverConditions\": [],\n" + + " \"parameterGroups\": {}\n" + + "}"; private static final FirebaseRemoteConfigException TEST_EXCEPTION = new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); @@ -80,6 +90,20 @@ public void testDefaultRemoteConfigClient() { assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } + @Test + public void testDefaultServerRemoteConfigClient() { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app); + + FirebaseRemoteConfigClient client = remoteConfig.getRemoteConfigClient(); + + assertTrue(client instanceof FirebaseRemoteConfigClientImpl); + assertSame(client, remoteConfig.getRemoteConfigClient()); + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getServerRemoteConfigUrl()); + } + @Test public void testAppDelete() { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); @@ -597,4 +621,54 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); return new FirebaseRemoteConfig(app, client); } + + // Get Server template tests + + @Test + public void testGetServerTemplate() throws FirebaseRemoteConfigException { + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ServerTemplate template = remoteConfig.getServerTemplate(); + String templateData = template.toJson(); + assertEquals(TEST_SERVER_TEMPLATE, templateData); + } + + @Test + public void testGetServerTemplateFailure() { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.getServerTemplate(); + } catch (FirebaseRemoteConfigException e) { + assertSame(TEST_EXCEPTION, e); + } + } + + @Test + public void testGetServerTemplateAsync() throws Exception { + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ServerTemplate template = remoteConfig.getServerTemplateAsync().get(); + String templateData = template.toJson(); + assertEquals(TEST_SERVER_TEMPLATE, templateData); + } + + @Test + public void testGetServerTemplateAsyncFailure() throws InterruptedException { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.getServerTemplateAsync().get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + } } diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java index 9ca58508d..1e7c0d470 100644 --- a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java +++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java @@ -21,28 +21,35 @@ public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{ private final Template resultTemplate; + private final String resultServerTemplate; private final FirebaseRemoteConfigException exception; private final ListVersionsResponse listVersionsResponse; private MockRemoteConfigClient(Template resultTemplate, + String resultServerTemplate, ListVersionsResponse listVersionsResponse, FirebaseRemoteConfigException exception) { this.resultTemplate = resultTemplate; + this.resultServerTemplate = resultServerTemplate; this.listVersionsResponse = listVersionsResponse; this.exception = exception; } static MockRemoteConfigClient fromTemplate(Template resultTemplate) { - return new MockRemoteConfigClient(resultTemplate, null, null); + return new MockRemoteConfigClient(resultTemplate,null, null, null); + } + + static MockRemoteConfigClient fromServerTemplate(String resultServerTemplate) { + return new MockRemoteConfigClient(null, resultServerTemplate,null, null); } static MockRemoteConfigClient fromListVersionsResponse( ListVersionsResponse listVersionsResponse) { - return new MockRemoteConfigClient(null, listVersionsResponse, null); + return new MockRemoteConfigClient(null,null, listVersionsResponse, null); } static MockRemoteConfigClient fromException(FirebaseRemoteConfigException exception) { - return new MockRemoteConfigClient(null, null, exception); + return new MockRemoteConfigClient(null,null, null, exception); } @Override @@ -53,6 +60,14 @@ public Template getTemplate() throws FirebaseRemoteConfigException { return resultTemplate; } + @Override + public String getServerTemplate() throws FirebaseRemoteConfigException { + if (exception != null) { + throw exception; + } + return resultServerTemplate; + } + @Override public Template getTemplateAtVersion(String versionNumber) throws FirebaseRemoteConfigException { if (exception != null) { @@ -87,3 +102,4 @@ public ListVersionsResponse listVersions( return listVersionsResponse; } } + From e8199c2e0bbbd41f4c345000e96dc8354d7f4cfd Mon Sep 17 00:00:00 2001 From: Athira M Date: Tue, 15 Jul 2025 20:44:45 +0530 Subject: [PATCH 02/29] Implement custom signal targeting for server side RC (#1108) Co-authored-by: Athira M --- .../firebase/remoteconfig/AndCondition.java | 2 - .../remoteconfig/ConditionEvaluator.java | 274 +++++++ .../remoteconfig/CustomSignalCondition.java | 140 ++++ .../remoteconfig/CustomSignalOperator.java | 54 ++ .../firebase/remoteconfig/KeysAndValues.java | 1 - .../firebase/remoteconfig/OneOfCondition.java | 19 + .../firebase/remoteconfig/OrCondition.java | 2 - .../firebase/remoteconfig/ServerConfig.java | 1 - .../firebase/remoteconfig/ServerTemplate.java | 10 +- .../remoteconfig/ServerTemplateData.java | 1 - .../remoteconfig/ServerTemplateImpl.java | 101 ++- .../remoteconfig/ConditionEvaluatorTest.java | 581 ++++++++++++++ .../FirebaseRemoteConfigClientImplTest.java | 758 +++++++++++++----- .../FirebaseRemoteConfigTest.java | 83 +- .../remoteconfig/MockRemoteConfigClient.java | 7 +- .../remoteconfig/ParameterValueTest.java | 54 -- .../remoteconfig/ServerConditionTest.java | 213 +++++ .../remoteconfig/ServerTemplateImplTest.java | 287 +++++++ .../firebase/remoteconfig/ValueTest.java | 104 +++ .../firebase/remoteconfig/VersionTest.java | 12 +- src/test/resources/getServerRemoteConfig.json | 107 +++ src/test/resources/getServerTemplateData.json | 97 +++ 22 files changed, 2609 insertions(+), 299 deletions(-) create mode 100644 src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java delete mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ValueTest.java create mode 100644 src/test/resources/getServerRemoteConfig.json create mode 100644 src/test/resources/getServerTemplateData.json diff --git a/src/main/java/com/google/firebase/remoteconfig/AndCondition.java b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java index 3be10560a..46e2e2100 100644 --- a/src/main/java/com/google/firebase/remoteconfig/AndCondition.java +++ b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java @@ -1,4 +1,3 @@ - /* * Copyright 2025 Google LLC * @@ -61,4 +60,3 @@ AndConditionResponse toAndConditionResponse() { .collect(Collectors.toList())); } } - diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java new file mode 100644 index 000000000..e333ca2b5 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -0,0 +1,274 @@ +/* + * Copyright 2025 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.IntPredicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ConditionEvaluator { + private static final int MAX_CONDITION_RECURSION_DEPTH = 10; + private static final Logger logger = LoggerFactory.getLogger(ConditionEvaluator.class); + + /** + * Evaluates server conditions and assigns a boolean value to each condition. + * + * @param conditions List of conditions which are to be evaluated. + * @param context A map with additional metadata used during evaluation. + * @return A map of condition to evaluated value. + */ + @NonNull + Map evaluateConditions( + @NonNull List conditions, + @Nullable KeysAndValues context) { + checkNotNull(conditions, "List of conditions must not be null."); + checkArgument(!conditions.isEmpty(), "List of conditions must not be empty."); + KeysAndValues evaluationContext = context != null + ? context + : new KeysAndValues.Builder().build(); + + Map evaluatedConditions = conditions.stream() + .collect(Collectors.toMap( + ServerCondition::getName, + condition -> + evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0) + )); + + return evaluatedConditions; + } + + private boolean evaluateCondition(OneOfCondition condition, KeysAndValues context, + int nestingLevel) { + if (nestingLevel > MAX_CONDITION_RECURSION_DEPTH) { + logger.warn("Maximum condition recursion depth exceeded."); + return false; + } + + if (condition.getOrCondition() != null) { + return evaluateOrCondition(condition.getOrCondition(), context, nestingLevel + 1); + } else if (condition.getAndCondition() != null) { + return evaluateAndCondition(condition.getAndCondition(), context, nestingLevel + 1); + } else if (condition.isTrue() != null) { + return true; + } else if (condition.isFalse() != null) { + return false; + } else if (condition.getCustomSignal() != null) { + return evaluateCustomSignalCondition(condition.getCustomSignal(), context); + } + logger.atWarn().log("Received invalid condition for evaluation."); + return false; + } + + + private boolean evaluateOrCondition(OrCondition condition, KeysAndValues context, + int nestingLevel) { + return condition.getConditions().stream() + .anyMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1)); + } + + private boolean evaluateAndCondition(AndCondition condition, KeysAndValues context, + int nestingLevel) { + return condition.getConditions().stream() + .allMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1)); + } + + private boolean evaluateCustomSignalCondition(CustomSignalCondition condition, + KeysAndValues context) { + CustomSignalOperator customSignalOperator = condition.getCustomSignalOperator(); + String customSignalKey = condition.getCustomSignalKey(); + ImmutableList targetCustomSignalValues = ImmutableList.copyOf( + condition.getTargetCustomSignalValues()); + + if (targetCustomSignalValues.isEmpty()) { + logger.warn(String.format( + "Values must be assigned to all custom signal fields. Operator:%s, Key:%s, Values:%s", + customSignalOperator, customSignalKey, targetCustomSignalValues)); + return false; + } + + String customSignalValue = context.get(customSignalKey); + if (customSignalValue == null) { + return false; + } + + switch (customSignalOperator) { + // String operations. + case STRING_CONTAINS: + return compareStrings(targetCustomSignalValues, customSignalValue, + (customSignal, targetSignal) -> customSignal.contains(targetSignal)); + case STRING_DOES_NOT_CONTAIN: + return !compareStrings(targetCustomSignalValues, customSignalValue, + (customSignal, targetSignal) -> customSignal.contains(targetSignal)); + case STRING_EXACTLY_MATCHES: + return compareStrings(targetCustomSignalValues, customSignalValue, + (customSignal, targetSignal) -> customSignal.equals(targetSignal)); + case STRING_CONTAINS_REGEX: + return compareStrings(targetCustomSignalValues, customSignalValue, + (customSignal, targetSignal) -> compareStringRegex(customSignal, targetSignal)); + + // Numeric operations. + case NUMERIC_LESS_THAN: + return compareNumbers(targetCustomSignalValues, customSignalValue, + (result) -> result < 0); + case NUMERIC_LESS_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, + (result) -> result <= 0); + case NUMERIC_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, + (result) -> result == 0); + case NUMERIC_NOT_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, + (result) -> result != 0); + case NUMERIC_GREATER_THAN: + return compareNumbers(targetCustomSignalValues, customSignalValue, + (result) -> result > 0); + case NUMERIC_GREATER_EQUAL: + return compareNumbers(targetCustomSignalValues, customSignalValue, + (result) -> result >= 0); + + // Semantic operations. + case SEMANTIC_VERSION_EQUAL: + return compareSemanticVersions(targetCustomSignalValues, customSignalValue, + (result) -> result == 0); + case SEMANTIC_VERSION_GREATER_EQUAL: + return compareSemanticVersions(targetCustomSignalValues, customSignalValue, + (result) -> result >= 0); + case SEMANTIC_VERSION_GREATER_THAN: + return compareSemanticVersions(targetCustomSignalValues, customSignalValue, + (result) -> result > 0); + case SEMANTIC_VERSION_LESS_EQUAL: + return compareSemanticVersions(targetCustomSignalValues, customSignalValue, + (result) -> result <= 0); + case SEMANTIC_VERSION_LESS_THAN: + return compareSemanticVersions(targetCustomSignalValues, customSignalValue, + (result) -> result < 0); + case SEMANTIC_VERSION_NOT_EQUAL: + return compareSemanticVersions(targetCustomSignalValues, customSignalValue, + (result) -> result != 0); + default: + return false; + } + } + + private boolean compareStrings(ImmutableList targetValues, String customSignal, + BiPredicate compareFunction) { + return targetValues.stream().anyMatch(targetValue -> + compareFunction.test(customSignal, targetValue)); + } + + private boolean compareStringRegex(String customSignal, String targetSignal) { + try { + return Pattern.compile(targetSignal).matcher(customSignal).matches(); + } catch (PatternSyntaxException e) { + return false; + } + } + + private boolean compareNumbers(ImmutableList targetValues, String customSignal, + IntPredicate compareFunction) { + if (targetValues.size() != 1) { + logger.warn(String.format( + "Target values must contain 1 element for numeric operations. Target Value: %s", + targetValues)); + return false; + } + + try { + double customSignalDouble = Double.parseDouble(customSignal); + double targetValue = Double.parseDouble(targetValues.get(0)); + int comparisonResult = Double.compare(customSignalDouble, targetValue); + return compareFunction.test(comparisonResult); + } catch (NumberFormatException e) { + logger.warn("Error parsing numeric values: customSignal=%s, targetValue=%s", + customSignal, targetValues.get(0), e); + return false; + } + } + + private boolean compareSemanticVersions(ImmutableList targetValues, + String customSignal, + IntPredicate compareFunction) { + if (targetValues.size() != 1) { + logger.warn(String.format("Target values must contain 1 element for semantic operation.")); + return false; + } + + String targetValueString = targetValues.get(0); + if (!validateSemanticVersion(targetValueString) + || !validateSemanticVersion(customSignal)) { + return false; + } + + List targetVersion = parseSemanticVersion(targetValueString); + List customSignalVersion = parseSemanticVersion(customSignal); + + int maxLength = 5; + if (targetVersion.size() > maxLength || customSignalVersion.size() > maxLength) { + logger.warn("Semantic version max length(%s) exceeded. Target: %s, Custom Signal: %s", + maxLength, targetValueString, customSignal); + return false; + } + + int comparison = compareSemanticVersions(customSignalVersion, targetVersion); + return compareFunction.test(comparison); + } + + private int compareSemanticVersions(List version1, List version2) { + int maxLength = Math.max(version1.size(), version2.size()); + int version1Size = version1.size(); + int version2Size = version2.size(); + + for (int i = 0; i < maxLength; i++) { + // Default to 0 if segment is missing + int v1 = i < version1Size ? version1.get(i) : 0; + int v2 = i < version2Size ? version2.get(i) : 0; + + int comparison = Integer.compare(v1, v2); + if (comparison != 0) { + return comparison; + } + } + // Versions are equal + return 0; + } + + private List parseSemanticVersion(String versionString) { + return Arrays.stream(versionString.split("\\.")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + private boolean validateSemanticVersion(String version) { + Pattern pattern = Pattern.compile("^[0-9]+(?:\\.[0-9]+){0,4}$"); + return pattern.matcher(version).matches(); + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java b/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java new file mode 100644 index 000000000..a8d96efdf --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/CustomSignalCondition.java @@ -0,0 +1,140 @@ +/* + * Copyright 2025 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.CustomSignalConditionResponse; + +import java.util.ArrayList; +import java.util.List; + +final class CustomSignalCondition { + private final String customSignalKey; + private final CustomSignalOperator customSignalOperator; + private final ImmutableList targetCustomSignalValues; + + public CustomSignalCondition( + @NonNull String customSignalKey, + @NonNull CustomSignalOperator customSignalOperator, + @NonNull List targetCustomSignalValues) { + checkArgument( + !Strings.isNullOrEmpty(customSignalKey), "Custom signal key must not be null or empty."); + checkNotNull(customSignalOperator); + checkNotNull(targetCustomSignalValues); + checkArgument( + !targetCustomSignalValues.isEmpty(), "Target custom signal values must not be empty."); + this.customSignalKey = customSignalKey.trim(); + this.customSignalOperator = customSignalOperator; + this.targetCustomSignalValues = ImmutableList.copyOf(targetCustomSignalValues); + } + + CustomSignalCondition(CustomSignalConditionResponse customSignalCondition) { + checkArgument( + !Strings.isNullOrEmpty(customSignalCondition.getKey()), + "Custom signal key must not be null or empty."); + checkArgument( + !customSignalCondition.getTargetValues().isEmpty(), + "Target custom signal values must not be empty."); + this.customSignalKey = customSignalCondition.getKey().trim(); + List targetCustomSignalValuesList = customSignalCondition.getTargetValues(); + this.targetCustomSignalValues = ImmutableList.copyOf(targetCustomSignalValuesList); + switch (customSignalCondition.getOperator()) { + case "NUMERIC_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_EQUAL; + break; + case "NUMERIC_GREATER_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_GREATER_EQUAL; + break; + case "NUMERIC_GREATER_THAN": + this.customSignalOperator = CustomSignalOperator.NUMERIC_GREATER_THAN; + break; + case "NUMERIC_LESS_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_LESS_EQUAL; + break; + case "NUMERIC_LESS_THAN": + this.customSignalOperator = CustomSignalOperator.NUMERIC_LESS_THAN; + break; + case "NUMERIC_NOT_EQUAL": + this.customSignalOperator = CustomSignalOperator.NUMERIC_NOT_EQUAL; + break; + case "SEMANTIC_VERSION_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_EQUAL; + break; + case "SEMANTIC_VERSION_GREATER_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL; + break; + case "SEMANTIC_VERSION_GREATER_THAN": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN; + break; + case "SEMANTIC_VERSION_LESS_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL; + break; + case "SEMANTIC_VERSION_LESS_THAN": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN; + break; + case "SEMANTIC_VERSION_NOT_EQUAL": + this.customSignalOperator = CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL; + break; + case "STRING_CONTAINS": + this.customSignalOperator = CustomSignalOperator.STRING_CONTAINS; + break; + case "STRING_CONTAINS_REGEX": + this.customSignalOperator = CustomSignalOperator.STRING_CONTAINS_REGEX; + break; + case "STRING_DOES_NOT_CONTAIN": + this.customSignalOperator = CustomSignalOperator.STRING_DOES_NOT_CONTAIN; + break; + case "STRING_EXACTLY_MATCHES": + this.customSignalOperator = CustomSignalOperator.STRING_EXACTLY_MATCHES; + break; + default: + this.customSignalOperator = CustomSignalOperator.UNSPECIFIED; + } + checkArgument( + this.customSignalOperator != CustomSignalOperator.UNSPECIFIED, + "Custom signal operator passed is invalid"); + } + + @NonNull + String getCustomSignalKey() { + return customSignalKey; + } + + @NonNull + CustomSignalOperator getCustomSignalOperator() { + return customSignalOperator; + } + + @NonNull + List getTargetCustomSignalValues() { + return new ArrayList<>(targetCustomSignalValues); + } + + CustomSignalConditionResponse toCustomConditonResponse() { + CustomSignalConditionResponse customSignalConditionResponse = + new CustomSignalConditionResponse(); + customSignalConditionResponse.setKey(this.customSignalKey); + customSignalConditionResponse.setOperator(this.customSignalOperator.getOperator()); + customSignalConditionResponse.setTargetValues(this.targetCustomSignalValues); + return customSignalConditionResponse; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java b/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java new file mode 100644 index 000000000..0c0924d62 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/CustomSignalOperator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025 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 com.google.firebase.internal.NonNull; + +enum CustomSignalOperator { + NUMERIC_EQUAL("NUMERIC_EQUAL"), + NUMERIC_GREATER_EQUAL("NUMERIC_GREATER_EQUAL"), + NUMERIC_GREATER_THAN("NUMERIC_GREATER_THAN"), + NUMERIC_LESS_EQUAL("NUMERIC_LESS_EQUAL"), + NUMERIC_LESS_THAN("NUMERIC_LESS_THAN"), + NUMERIC_NOT_EQUAL("NUMERIC_NOT_EQUAL"), + SEMANTIC_VERSION_EQUAL("SEMANTIC_VERSION_EQUAL"), + SEMANTIC_VERSION_GREATER_EQUAL("SEMANTIC_VERSION_GREATER_EQUAL"), + SEMANTIC_VERSION_GREATER_THAN("SEMANTIC_VERSION_GREATER_THAN"), + SEMANTIC_VERSION_LESS_EQUAL("SEMANTIC_VERSION_LESS_EQUAL"), + SEMANTIC_VERSION_LESS_THAN("SEMANTIC_VERSION_LESS_THAN"), + SEMANTIC_VERSION_NOT_EQUAL("SEMANTIC_VERSION_NOT_EQUAL"), + STRING_CONTAINS("STRING_CONTAINS"), + STRING_CONTAINS_REGEX("STRING_CONTAINS_REGEX"), + STRING_DOES_NOT_CONTAIN("STRING_DOES_NOT_CONTAIN"), + STRING_EXACTLY_MATCHES("STRING_EXACTLY_MATCHES"), + UNSPECIFIED("CUSTOM_SIGNAL_OPERATOR_UNSPECIFIED"); + + private final String operator; + + CustomSignalOperator(@NonNull String operator) { + checkArgument(!Strings.isNullOrEmpty(operator), "Operator must not be null or empty."); + this.operator = operator; + } + + @NonNull + String getOperator() { + return operator; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java index 97064d96a..47b159bf7 100644 --- a/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java +++ b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java @@ -1,4 +1,3 @@ - /* * Copyright 2025 Google LLC * diff --git a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java index 255cee565..7e352ec0d 100644 --- a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java +++ b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java @@ -26,6 +26,7 @@ class OneOfCondition { private OrCondition orCondition; private AndCondition andCondition; + private CustomSignalCondition customSignal; private String trueValue; private String falseValue; @@ -36,6 +37,10 @@ class OneOfCondition { if (oneOfconditionResponse.getAndCondition() != null) { this.andCondition = new AndCondition(oneOfconditionResponse.getAndCondition()); } + if (oneOfconditionResponse.getCustomSignalCondition() != null) { + this.customSignal = + new CustomSignalCondition(oneOfconditionResponse.getCustomSignalCondition()); + } } @VisibleForTesting @@ -66,6 +71,11 @@ String isFalse() { return falseValue; } + @Nullable + CustomSignalCondition getCustomSignal() { + return customSignal; + } + OneOfCondition setOrCondition(@NonNull OrCondition orCondition) { checkNotNull(orCondition, "`Or` condition cannot be set to null."); this.orCondition = orCondition; @@ -78,6 +88,12 @@ OneOfCondition setAndCondition(@NonNull AndCondition andCondition) { return this; } + OneOfCondition setCustomSignal(@NonNull CustomSignalCondition customSignal) { + checkNotNull(customSignal, "`Custom signal` condition cannot be set to null."); + this.customSignal = customSignal; + return this; + } + OneOfCondition setTrue() { this.trueValue = "true"; return this; @@ -96,6 +112,9 @@ OneOfConditionResponse toOneOfConditionResponse() { if (this.orCondition != null) { oneOfConditionResponse.setOrCondition(this.orCondition.toOrConditionResponse()); } + if (this.customSignal != null) { + oneOfConditionResponse.setCustomSignalCondition(this.customSignal.toCustomConditonResponse()); + } return oneOfConditionResponse; } } diff --git a/src/main/java/com/google/firebase/remoteconfig/OrCondition.java b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java index 4d258f23b..2842ec1e1 100644 --- a/src/main/java/com/google/firebase/remoteconfig/OrCondition.java +++ b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java @@ -1,4 +1,3 @@ - /* * Copyright 2025 Google LLC * @@ -58,4 +57,3 @@ OrConditionResponse toOrConditionResponse() { .collect(Collectors.toList())); } } - diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java index 9e063009f..8540578b0 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java @@ -100,4 +100,3 @@ private Value getValue(String key) { return new Value(ValueSource.STATIC); } } - diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java index f16992744..26ab104f4 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java @@ -28,10 +28,18 @@ public interface Builder { ServerTemplate build(); } /** + * Proccess the template data with a condition evaluator + * based on the provided context. + */ + ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigException; + /** + * Proccess the template data without context. + */ + ServerConfig evaluate() throws FirebaseRemoteConfigException; + /** * Fetches and caches the current active version of the project. */ ApiFuture load() throws FirebaseRemoteConfigException; String toJson(); } - diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java index 34674855f..59d51b51a 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java @@ -1,4 +1,3 @@ - /* * Copyright 2025 Google LLC * diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java index 81c71e898..c85fa160b 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -1,4 +1,3 @@ - /* * Copyright 2025 Google LLC * @@ -19,18 +18,27 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.ErrorCode; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public final class ServerTemplateImpl implements ServerTemplate { private final KeysAndValues defaultConfig; private FirebaseRemoteConfigClient client; private ServerTemplateData cache; private final AtomicReference cachedTemplate; // Added field for cached template - + private static final Logger logger = LoggerFactory.getLogger(ServerTemplate.class); + public static class Builder implements ServerTemplate.Builder { private KeysAndValues defaultConfig; private String cachedTemplate; @@ -69,6 +77,36 @@ private ServerTemplateImpl(Builder builder) { } } + @Override + public ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigException { + if (this.cache == null) { + throw new FirebaseRemoteConfigException(ErrorCode.FAILED_PRECONDITION, + "No Remote Config Server template in cache. Call load() before calling evaluate()."); + } + + Map configValues = new HashMap<>(); + ImmutableMap defaultConfigValues = defaultConfig.keysAndValues; + // Initializes config Value objects with default values. + for (String configName : defaultConfigValues.keySet()) { + configValues.put(configName, new Value(ValueSource.DEFAULT, + defaultConfigValues.get(configName))); + } + + ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); + ImmutableMap evaluatedCondition = ImmutableMap.copyOf( + conditionEvaluator.evaluateConditions(cache.getServerConditions(), context)); + ImmutableMap parameters = ImmutableMap.copyOf(cache.getParameters()); + mergeDerivedConfigValues(evaluatedCondition, parameters, configValues); + + return new ServerConfig(configValues); + } + + @Override + public ServerConfig evaluate() throws FirebaseRemoteConfigException { + KeysAndValues context = new KeysAndValues.Builder().build(); + return evaluate(context); + } + @Override public ApiFuture load() throws FirebaseRemoteConfigException { String serverTemplate = client.getServerTemplate(); @@ -91,5 +129,62 @@ public String toJson() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); return gson.toJson(this.cache); } -} + private void mergeDerivedConfigValues(ImmutableMap evaluatedCondition, + ImmutableMap parameters, Map configValues) { + // Overlays config Value objects derived by evaluating the template. + for (String parameterName : parameters.keySet()) { + Parameter parameter = parameters.get(parameterName); + if (parameter == null) { + logger.warn(String.format("Parameter value is not assigned for %s", parameterName)); + continue; + } + + ImmutableMap conditionalValues = ImmutableMap.copyOf( + parameter.getConditionalValues()); + ParameterValue derivedValue = null; + + // Iterates in order over condition list. If there is a value associated + // with a condition, this checks if the condition is true. + for (String conditionName : evaluatedCondition.keySet()) { + boolean conditionEvaluation = evaluatedCondition.get(conditionName); + if (conditionalValues.containsKey(conditionName) && conditionEvaluation) { + derivedValue = conditionalValues.get(conditionName); + break; + } + } + + if (derivedValue != null && derivedValue.toParameterValueResponse().isUseInAppDefault()) { + logger.warn( + String.format("Derived value found for %s but parameter is set to use in app default.", + parameterName)); + continue; + } + + if (derivedValue != null) { + String parameterValue = derivedValue.toParameterValueResponse().getValue(); + Value value = new Value(ValueSource.REMOTE, parameterValue); + configValues.put(parameterName, value); + continue; + } + + ParameterValue defaultValue = parameter.getDefaultValue(); + if (defaultValue == null) { + logger.warn(String.format("Default parameter value for %s is not set.", + parameterName)); + continue; + } + + ParameterValueResponse defaultValueResponse = defaultValue.toParameterValueResponse(); + if (defaultValueResponse != null && defaultValueResponse.isUseInAppDefault()) { + logger.info(String.format("Default value for %s is set to use in app default.", + parameterName)); + continue; + } + + String parameterDefaultValue = defaultValue.toParameterValueResponse().getValue(); + Value value = new Value(ValueSource.REMOTE, parameterDefaultValue); + configValues.put(parameterName, value); + } + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java new file mode 100644 index 000000000..e2277c7e4 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java @@ -0,0 +1,581 @@ +/* + * Copyright 2025 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.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; + +import java.util.Arrays; +import java.util.Map; + +import org.junit.Test; + +public class ConditionEvaluatorTest { + + private final ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); + + @Test + public void testEvaluateConditionsEmptyOrConditionThrowsException() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> createOneOfOrCondition(null)); + assertEquals("List of conditions for OR operation must not be empty.", error.getMessage()); + } + + @Test + public void testEvaluateConditionsEmptyOrAndConditionThrowsException() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> createOneOfAndCondition(null)); + assertEquals("List of conditions for AND operation must not be empty.", error.getMessage()); + } + + @Test + public void testEvaluateConditionsOrAndTrueToTrue() { + OneOfCondition oneOfConditionTrue = createOneOfTrueCondition(); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionTrue); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues context = new KeysAndValues.Builder().build(); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + context); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsOrAndFalseToFalse() { + OneOfCondition oneOfConditionFalse = createOneOfFalseCondition(); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionFalse); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues context = new KeysAndValues.Builder().build(); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + context); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsNonOrTopConditionToTrue() { + OneOfCondition oneOfConditionTrue = createOneOfTrueCondition(); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionTrue); + KeysAndValues context = new KeysAndValues.Builder().build(); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + context); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.01"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalInvalidValueNumericOperationToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_THAN, ImmutableList.of("non-numeric")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.01"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericLessEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_LESS_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-49.9"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericEqualsToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericEqualsToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.000001"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericNotEqualsToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_NOT_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericNotEqualsToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_NOT_EQUAL, ImmutableList.of("50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.000001"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericGreaterEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericGreaterEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_EQUAL, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.01"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericgreaterThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-50.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalNumericGreaterThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.NUMERIC_GREATER_THAN, ImmutableList.of("-50.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "-49.09"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hudred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringDoesNotContainToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_DOES_NOT_CONTAIN, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hudred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringDoesNotContainToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_DOES_NOT_CONTAIN, ImmutableList.of("One", "hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringExactlyMatchesToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_EXACTLY_MATCHES, ImmutableList.of("hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringExactlyMatchesToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_EXACTLY_MATCHES, ImmutableList.of("hundred")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsRegexToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of(".*hund.*")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "hundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsRegexToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of("$hund.*")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two ahundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionsCustomSignalStringContainsInvalidRegexToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.STRING_CONTAINS_REGEX, ImmutableList.of("abc)")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "Two ahundred"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.2.0.1"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessThanInvalidVersionToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN, ImmutableList.of("50.0.-20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.2.0.1"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL, ImmutableList.of("50.0.20.0.0")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticLessEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL, ImmutableList.of("50.0.2")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.2.1.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterThanToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN, ImmutableList.of("50.0.2")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.1"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterThanToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticGreaterEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL, ImmutableList.of("50.0.20.1")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_EQUAL, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_EQUAL, ImmutableList.of("50.0.20.1")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticNotEqualToTrue() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL, ImmutableList.of("50.0.20.1")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("signal_key")); + } + + @Test + public void testEvaluateConditionCustomSignalSemanticNotEqualToFalse() { + ServerCondition condition = createCustomSignalServerCondition( + CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL, ImmutableList.of("50.0.20")); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("signal_key", "50.0.20.0.0"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("signal_key")); + } + + private ServerCondition createCustomSignalServerCondition( + CustomSignalOperator operator, + ImmutableList targetCustomSignalValues) { + CustomSignalCondition condition = new CustomSignalCondition("signal_key", operator, + targetCustomSignalValues); + OneOfCondition oneOfConditionCustomSignal = new OneOfCondition(); + oneOfConditionCustomSignal.setCustomSignal(condition); + return new ServerCondition("signal_key", oneOfConditionCustomSignal); + } + + private OneOfCondition createOneOfOrCondition(OneOfCondition condition) { + OrCondition orCondition = condition != null ? new OrCondition(ImmutableList.of(condition)) + : new OrCondition(ImmutableList.of()); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setOrCondition(orCondition); + return oneOfCondition; + } + + private OneOfCondition createOneOfAndCondition(OneOfCondition condition) { + AndCondition andCondition = condition != null ? new AndCondition(ImmutableList.of(condition)) + : new AndCondition(ImmutableList.of()); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setAndCondition(andCondition); + return oneOfCondition; + } + + private OneOfCondition createOneOfTrueCondition() { + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setTrue(); + return oneOfCondition; + } + + private OneOfCondition createOneOfFalseCondition() { + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setFalse(); + return oneOfCondition; + } +} \ No newline at end of file diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index aea2dd1eb..e92526555 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -25,7 +25,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpMethods; @@ -42,28 +41,30 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.OutgoingHttpRequest; - import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.SdkUtils; import com.google.firebase.remoteconfig.internal.TemplateResponse; import com.google.firebase.testing.TestResponseInterceptor; import com.google.firebase.testing.TestUtils; - +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URLDecoder; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; - import org.junit.Before; import org.junit.Test; public class FirebaseRemoteConfigClientImplTest { private static final String TEST_REMOTE_CONFIG_URL = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + private static final String TEST_SERVER_REMOTE_CONFIG_URL = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; private static final List HTTP_STATUS_CODES = ImmutableList.of(401, 404, 500); @@ -74,57 +75,113 @@ public class FirebaseRemoteConfigClientImplTest { private static final String MOCK_TEMPLATE_RESPONSE = TestUtils .loadResource("getRemoteConfig.json"); + + private static final String MOCK_SERVER_TEMPLATE_RESPONSE = TestUtils + .loadResource("getServerRemoteConfig.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( - "welcome_message_text", new Parameter() + private static final Map EXPECTED_PARAMETERS = + ImmutableMap.of( + "welcome_message_text", + new Parameter() .setDefaultValue(ParameterValue.of("welcome to app")) - .setConditionalValues(ImmutableMap.of( - "ios_en", ParameterValue.of("welcome to app en") - )) + .setConditionalValues( + ImmutableMap.of( + "ios_en", ParameterValue.of("welcome to app en"))) .setDescription("text for welcome message!") .setValueType(ParameterValueType.STRING), - "header_text", new Parameter() + "header_text", + new Parameter() .setDefaultValue(ParameterValue.inAppDefault()) - .setValueType(ParameterValueType.STRING) - ); - - private static final Map EXPECTED_PARAMETER_GROUPS = ImmutableMap.of( - "new menu", new ParameterGroup() - .setDescription("New Menu") - .setParameters(ImmutableMap.of( - "pumpkin_spice_season", new Parameter() - .setDefaultValue(ParameterValue.of("true")) - .setDescription("Whether it's currently pumpkin spice season.") - .setValueType(ParameterValueType.BOOLEAN) - ) - ) - ); - - private static final List EXPECTED_CONDITIONS = ImmutableList.of( + .setValueType(ParameterValueType.STRING)); + + private static final Map EXPECTED_PARAMETER_GROUPS = + ImmutableMap.of( + "new menu", + new ParameterGroup() + .setDescription("New Menu") + .setParameters( + ImmutableMap.of( + "pumpkin_spice_season", + new Parameter() + .setDefaultValue(ParameterValue.of("true")) + .setDescription("Whether it's currently pumpkin spice season.") + .setValueType(ParameterValueType.BOOLEAN)))); + + private static final List EXPECTED_CONDITIONS = + ImmutableList.of( new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']") - .setTagColor(TagColor.INDIGO), - new Condition("android_en", - "device.os == 'android' && device.country in ['us', 'uk']") - ); - - private static final Version EXPECTED_VERSION = new Version(new TemplateResponse.VersionResponse() - .setVersionNumber("17") - .setUpdateOrigin("ADMIN_SDK_NODE") - .setUpdateType("INCREMENTAL_UPDATE") - .setUpdateUser(new TemplateResponse.UserResponse() - .setEmail("firebase-user@account.com") - .setName("dev-admin") - .setImageUrl("http://image.jpg")) - .setUpdateTime("2020-11-15T06:57:26.342763941Z") - .setDescription("promo config") - ); - - private static final Template EXPECTED_TEMPLATE = new Template() + .setTagColor(TagColor.INDIGO), + new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']")); + + private static final List EXPECTED_SERVER_CONDITIONS = + ImmutableList.of( + new ServerCondition("custom_signal", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100"))))))))))), + new ServerCondition("chained_conditions", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))), + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "premium users", + CustomSignalOperator + .NUMERIC_GREATER_THAN, + new ArrayList<>( + ImmutableList.of("20")))) + )))))))); + + private static final Version EXPECTED_VERSION = + new Version( + new TemplateResponse.VersionResponse() + .setVersionNumber("17") + .setUpdateOrigin("ADMIN_SDK_NODE") + .setUpdateType("INCREMENTAL_UPDATE") + .setUpdateUser( + new TemplateResponse.UserResponse() + .setEmail("firebase-user@account.com") + .setName("dev-admin") + .setImageUrl("http://image.jpg")) + .setUpdateTime("2020-11-15T06:57:26.342763941Z") + .setDescription("promo config")); + + private static final Template EXPECTED_TEMPLATE = + new Template() .setETag(TEST_ETAG) .setParameters(EXPECTED_PARAMETERS) .setConditions(EXPECTED_CONDITIONS) @@ -159,16 +216,19 @@ public void testGetTemplate() throws Exception { @Test public void testGetTemplateWithTimestampUpToNanosecondPrecision() throws Exception { - List timestamps = ImmutableList.of( + List timestamps = + ImmutableList.of( "2020-11-15T06:57:26.342Z", "2020-11-15T06:57:26.342763Z", - "2020-11-15T06:57:26.342763941Z" - ); + "2020-11-15T06:57:26.342763941Z"); for (String timestamp : timestamps) { response.addHeader("etag", TEST_ETAG); - String templateResponse = "{\"version\": {" + String templateResponse = + "{\"version\": {" + " \"versionNumber\": \"17\"," - + " \"updateTime\": \"" + timestamp + "\"" + + " \"updateTime\": \"" + + timestamp + + "\"" + " }}"; response.setContent(templateResponse); @@ -222,8 +282,12 @@ public void testGetTemplateHttpError() { client.getTemplate(); 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -238,8 +302,8 @@ public void testGetTemplateTransportError() { 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()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -272,8 +336,12 @@ public void testGetTemplateErrorWithZeroContentResponse() { client.getTemplate(); 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -288,8 +356,12 @@ public void testGetTemplateErrorWithMalformedResponse() { client.getTemplate(); 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -340,8 +412,9 @@ public void testGetTemplateAtVersionWithNullString() throws Exception { @Test public void testGetTemplateAtVersionWithInvalidString() throws Exception { - List invalidVersionStrings = ImmutableList - .of("", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = + ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -408,8 +481,12 @@ public void testGetTemplateAtVersionHttpError() { client.getTemplateAtVersion("24"); 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -424,8 +501,8 @@ public void testGetTemplateAtVersionTransportError() { 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()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -458,8 +535,12 @@ public void testGetTemplateAtVersionErrorWithZeroContentResponse() { client.getTemplateAtVersion("24"); 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -474,8 +555,12 @@ public void testGetTemplateAtVersionErrorWithMalformedResponse() { client.getTemplateAtVersion("24"); 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -484,15 +569,17 @@ public void testGetTemplateAtVersionErrorWithMalformedResponse() { @Test public void testGetTemplateAtVersionErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.getTemplateAtVersion("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -501,17 +588,22 @@ public void testGetTemplateAtVersionErrorWithDetails() { @Test public void testGetTemplateAtVersionErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.getTemplateAtVersion("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), "?versionNumber=24"); } @@ -554,7 +646,8 @@ public void testPublishTemplateWithValidTemplateAndForceTrue() throws Exception public void testPublishTemplateWithValidTemplateAndValidateOnlyTrue() throws Exception { response.addHeader("etag", TEST_ETAG); response.setContent(MOCK_TEMPLATE_RESPONSE); - Template expectedTemplate = new Template() + Template expectedTemplate = + new Template() .setETag("etag-123456789012-45") .setParameters(EXPECTED_PARAMETERS) .setConditions(EXPECTED_CONDITIONS) @@ -567,8 +660,8 @@ public void testPublishTemplateWithValidTemplateAndValidateOnlyTrue() throws Exc assertNotEquals(TEST_ETAG, validatedTemplate.getETag()); assertEquals("etag-123456789012-45", validatedTemplate.getETag()); assertEquals(expectedTemplate, validatedTemplate); - checkPutRequestHeader(interceptor.getLastRequest(), "?validateOnly=true", - "etag-123456789012-45"); + checkPutRequestHeader( + interceptor.getLastRequest(), "?validateOnly=true", "etag-123456789012-45"); } @Test @@ -612,8 +705,12 @@ public void testPublishTemplateHttpError() { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); 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.PUT); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -628,8 +725,8 @@ public void testPublishTemplateTransportError() { 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()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -662,8 +759,12 @@ public void testPublishTemplateErrorWithZeroContentResponse() { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); 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.PUT); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -678,8 +779,12 @@ public void testPublishTemplateErrorWithMalformedResponse() { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); 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.PUT); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -688,15 +793,17 @@ public void testPublishTemplateErrorWithMalformedResponse() { @Test public void testPublishTemplateErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.PUT); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -705,17 +812,22 @@ public void testPublishTemplateErrorWithDetails() { @Test public void testPublishTemplateErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.publishTemplate(new Template().setETag(TEST_ETAG), false, false); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.PUT); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.PUT); } checkPutRequestHeader(interceptor.getLastRequest()); } @@ -730,8 +842,9 @@ public void testRollbackWithNullString() throws Exception { @Test public void testRollbackWithInvalidString() throws Exception { - List invalidVersionStrings = ImmutableList - .of("", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = + ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -755,8 +868,8 @@ public void testRollbackWithValidString() throws Exception { assertEquals(EXPECTED_TEMPLATE, rolledBackTemplate); assertEquals(1605423446000L, rolledBackTemplate.getVersion().getUpdateTime()); checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } @Test @@ -772,8 +885,8 @@ public void testRollbackWithEmptyTemplateResponse() throws Exception { assertEquals(0, template.getParameterGroups().size()); assertNull(template.getVersion()); checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } @Test(expected = IllegalStateException.class) @@ -802,12 +915,16 @@ public void testRollbackHttpError() throws IOException { client.rollback("24"); 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.POST); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @@ -820,8 +937,8 @@ public void testRollbackTransportError() { 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()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -843,8 +960,8 @@ public void testRollbackSuccessResponseWithUnexpectedPayload() throws IOExceptio assertNull(error.getRemoteConfigErrorCode()); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } @Test @@ -856,12 +973,16 @@ public void testRollbackErrorWithZeroContentResponse() throws IOException { client.rollback("24"); 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.POST); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @@ -874,52 +995,63 @@ public void testRollbackErrorWithMalformedResponse() throws IOException { client.rollback("24"); 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.POST); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @Test public void testRollbackErrorWithDetails() throws IOException { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.rollback("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.POST); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @Test public void testRollbackErrorWithRcError() throws IOException { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.rollback("24"); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.POST); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.POST); } checkPostRequestHeader(interceptor.getLastRequest(), ":rollback"); - checkRequestContent(interceptor.getLastRequest(), - ImmutableMap.of("versionNumber", "24")); + checkRequestContent( + interceptor.getLastRequest(), ImmutableMap.of("versionNumber", "24")); } } @@ -941,33 +1073,36 @@ public void testListVersionsWithNullOptions() throws Exception { public void testListVersionsWithOptions() throws Exception { response.setContent(MOCK_LIST_VERSIONS_RESPONSE); - TemplateResponse.ListVersionsResponse versionsList = client.listVersions( + TemplateResponse.ListVersionsResponse versionsList = + client.listVersions( ListVersionsOptions.builder() - .setPageSize(10) - .setPageToken("token") - .setStartTimeMillis(1605219122000L) - .setEndTimeMillis(1606245035000L) - .setEndVersionNumber("29").build()); + .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( + 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" - ); + "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")); + actualQuery.put( + URLDecoder.decode(pair.substring(0, idx), "UTF-8"), + URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); } assertEquals("GET", request.getRequestMethod()); @@ -999,8 +1134,12 @@ public void testListVersionsHttpError() { 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\n{}", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1015,8 +1154,8 @@ public void testListVersionsTransportError() { 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()); + assertEquals( + "Unknown error while making a remote service call: transport error", error.getMessage()); assertTrue(error.getCause() instanceof IOException); assertNull(error.getHttpResponse()); assertNull(error.getRemoteConfigErrorCode()); @@ -1049,8 +1188,12 @@ public void testListVersionsErrorWithZeroContentResponse() { 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnull", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1065,8 +1208,12 @@ public void testListVersionsErrorWithMalformedResponse() { 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); + checkExceptionFromHttpResponse( + error, + HTTP_STATUS_TO_ERROR_CODE.get(code), + null, + "Unexpected HTTP response with status: " + code + "\nnot json", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1075,15 +1222,17 @@ public void testListVersionsErrorWithMalformedResponse() { @Test public void testListVersionsErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + 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); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1092,17 +1241,22 @@ public void testListVersionsErrorWithDetails() { @Test public void testListVersionsErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( + response + .setStatusCode(code) + .setContent( "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + "\"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); + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest(), ":listVersions"); } @@ -1127,7 +1281,8 @@ public void testBuilderNullRequestFactory() { @Test public void testFromApp() throws IOException { - FirebaseOptions options = FirebaseOptions.builder() + FirebaseOptions options = + FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -1139,8 +1294,8 @@ public void testFromApp() throws IOException { assertEquals(TEST_REMOTE_CONFIG_URL, client.getRemoteConfigUrl()); assertSame(options.getJsonFactory(), client.getJsonFactory()); - HttpRequest request = client.getRequestFactory().buildGetRequest( - new GenericUrl("https://example.com")); + HttpRequest request = + client.getRequestFactory().buildGetRequest(new GenericUrl("https://example.com")); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } finally { app.delete(); @@ -1148,33 +1303,32 @@ public void testFromApp() throws IOException { } private FirebaseRemoteConfigClientImpl initRemoteConfigClient( - MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { - MockHttpTransport transport = new MockHttpTransport.Builder() - .setLowLevelHttpResponse(mockResponse) - .build(); + MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { + MockHttpTransport transport = + new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build(); return FirebaseRemoteConfigClientImpl.builder() - .setProjectId("test-project") - .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(transport.createRequestFactory()) - .setResponseInterceptor(interceptor) - .build(); + .setProjectId("test-project") + .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .setResponseInterceptor(interceptor) + .build(); } private FirebaseRemoteConfigClientImpl initClientWithFaultyTransport() { HttpTransport transport = TestUtils.createFaultyHttpTransport(); return FirebaseRemoteConfigClientImpl.builder() - .setProjectId("test-project") - .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(transport.createRequestFactory()) - .build(); + .setProjectId("test-project") + .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) + .setRequestFactory(transport.createRequestFactory()) + .build(); } private FirebaseRemoteConfigClientImpl.Builder fullyPopulatedBuilder() { return FirebaseRemoteConfigClientImpl.builder() - .setProjectId("test-project") - .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) - .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); + .setProjectId("test-project") + .setJsonFactory(ApiClientUtils.getDefaultJsonFactory()) + .setRequestFactory(ApiClientUtils.getDefaultTransport().createRequestFactory()); } private void checkGetRequestHeader(HttpRequest request) { @@ -1190,6 +1344,19 @@ private void checkGetRequestHeader(HttpRequest request, String urlSuffix) { assertEquals("gzip", headers.getAcceptEncoding()); } + private void checkGetRequestHeaderForServer(HttpRequest request) { + checkGetRequestHeaderForServer(request, ""); + } + + private void checkGetRequestHeaderForServer(HttpRequest request, String urlSuffix) { + assertEquals("GET", request.getRequestMethod()); + assertEquals(TEST_SERVER_REMOTE_CONFIG_URL + urlSuffix, request.getUrl().toString()); + HttpHeaders headers = request.getHeaders(); + assertEquals("fire-admin-java/" + SdkUtils.getVersion(), headers.get("X-Firebase-Client")); + assertEquals(SdkUtils.getMetricsHeader(), request.getHeaders().get("X-Goog-Api-Client")); + assertEquals("gzip", headers.getAcceptEncoding()); + } + private void checkPutRequestHeader(HttpRequest request) { checkPutRequestHeader(request, "", TEST_ETAG); } @@ -1213,8 +1380,8 @@ private void checkPostRequestHeader(HttpRequest request, String urlSuffix) { assertEquals("gzip", headers.getAcceptEncoding()); } - private void checkRequestContent( - HttpRequest request, Map expected) throws IOException { + private void checkRequestContent(HttpRequest request, Map expected) + throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); request.getContent().writeTo(out); JsonParser parser = ApiClientUtils.getDefaultJsonFactory().createJsonParser(out.toString()); @@ -1224,11 +1391,11 @@ private void checkRequestContent( } private void checkExceptionFromHttpResponse( - FirebaseRemoteConfigException error, - ErrorCode expectedCode, - RemoteConfigErrorCode expectedRemoteConfigCode, - String expectedMessage, - String httpMethod) { + FirebaseRemoteConfigException error, + ErrorCode expectedCode, + RemoteConfigErrorCode expectedRemoteConfigCode, + String expectedMessage, + String httpMethod) { assertEquals(expectedCode, error.getErrorCode()); assertEquals(expectedMessage, error.getMessage()); assertTrue(error.getCause() instanceof HttpResponseException); @@ -1239,5 +1406,226 @@ private void checkExceptionFromHttpResponse( assertEquals(httpMethod, request.getMethod()); assertTrue(request.getUrl().startsWith("https://firebaseremoteconfig.googleapis.com")); } -} + // Get server template tests + + @Test + public void testGetServerTemplate() throws Exception { + response.addHeader("etag", TEST_ETAG); + response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE); + + String receivedTemplate = client.getServerTemplate(); + ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate); + + assertEquals(EXPECTED_PARAMETERS, serverTemplateData.getParameters()); + assertEquals(TEST_ETAG, serverTemplateData.getETag()); + assertEquals( + convertObjectToString(EXPECTED_SERVER_CONDITIONS), + convertObjectToString(serverTemplateData.getServerConditions())); + assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + + @Test + public void testGetServerTemplateWithTimestampUpToNanosecondPrecision() throws Exception { + List timestamps = + ImmutableList.of( + "2020-11-15T06:57:26.342Z", + "2020-11-15T06:57:26.342763Z", + "2020-11-15T06:57:26.342763941Z"); + for (String timestamp : timestamps) { + response.addHeader("etag", TEST_ETAG); + String templateResponse = + "{\"version\": {" + + " \"versionNumber\": \"17\"," + + " \"updateTime\": \"" + + timestamp + + "\"" + + " }}"; + response.setContent(templateResponse); + + String receivedTemplate = client.getServerTemplate(); + ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate); + + assertEquals(TEST_ETAG, serverTemplateData.getETag()); + assertEquals("17", serverTemplateData.getVersion().getVersionNumber()); + assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateWithEmptyTemplateResponse() throws Exception { + response.addHeader("etag", TEST_ETAG); + response.setContent("{}"); + + String receivedTemplate = client.getServerTemplate(); + ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate); + + assertEquals(TEST_ETAG, serverTemplateData.getETag()); + assertEquals(0, serverTemplateData.getParameters().size()); + assertEquals(0, serverTemplateData.getServerConditions().size()); + assertEquals(0, serverTemplateData.getParameterGroups().size()); + assertNull(serverTemplateData.getVersion()); + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + + @Test(expected = IllegalStateException.class) + public void testGetServerTemplateWithNoEtag() throws FirebaseRemoteConfigException { + // ETag does not exist + response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE); + + client.getServerTemplate(); + } + + @Test(expected = IllegalStateException.class) + public void testGetServerTemplateWithEmptyEtag() throws FirebaseRemoteConfigException { + // Empty ETag + response.addHeader("etag", ""); + response.setContent(MOCK_SERVER_TEMPLATE_RESPONSE); + + client.getServerTemplate(); + } + + @Test + public void testGetServerTemplateHttpError() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("{}"); + + try { + client.getServerTemplate(); + 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); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateTransportError() { + client = initClientWithFaultyTransport(); + + try { + client.getServerTemplate(); + 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 testGetServerTemplateSuccessResponseWithUnexpectedPayload() { + response.setContent("not valid json"); + + try { + client.getServerTemplate(); + 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()); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + + @Test + public void testGetServerTemplateErrorWithZeroContentResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setZeroContent(); + + try { + client.getServerTemplate(); + 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); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateErrorWithMalformedResponse() { + for (int code : HTTP_STATUS_CODES) { + response.setStatusCode(code).setContent("not json"); + + try { + client.getServerTemplate(); + 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); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateErrorWithDetails() { + for (int code : HTTP_STATUS_CODES) { + response + .setStatusCode(code) + .setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + @Test + public void testGetServerTemplateErrorWithRcError() { + for (int code : HTTP_STATUS_CODES) { + response + .setStatusCode(code) + .setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + + try { + client.getServerTemplate(); + fail("No error thrown for HTTP error"); + } catch (FirebaseRemoteConfigException error) { + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); + } + checkGetRequestHeaderForServer(interceptor.getLastRequest()); + } + } + + public static String convertObjectToString(Object object) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); // Optional: pretty printing + return gson.toJson(object); + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 66821abb8..f34035f55 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -27,20 +27,18 @@ import com.google.firebase.FirebaseOptions; 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; import org.junit.Test; /** Unit tests - * for {@link FirebaseRemoteConfig}. - * */ +* for {@link FirebaseRemoteConfig}. +* */ public class FirebaseRemoteConfigTest { - private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + private static final FirebaseOptions TEST_OPTIONS = + FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); @@ -52,7 +50,7 @@ public class FirebaseRemoteConfigTest { + " \"parameterGroups\": {}\n" + "}"; private static final FirebaseRemoteConfigException TEST_EXCEPTION = - new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); + new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); @After public void tearDown() { @@ -86,7 +84,8 @@ public void testDefaultRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } @@ -122,16 +121,16 @@ public void testAppDelete() { @Test public void testRemoteConfigClientWithoutProjectId() { - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .build(); + FirebaseOptions options = + FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); FirebaseApp.initializeApp(options); try { FirebaseRemoteConfig.getInstance(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { - String message = "Project ID is required to access Remote Config service. Use a service " + String message = + "Project ID is required to access Remote Config service. Use a service " + "account credential or set the project ID explicitly via FirebaseOptions. " + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " + "environment variable."; @@ -145,8 +144,8 @@ public void testRemoteConfigClientWithoutProjectId() { @Test public void testGetTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplate(); @@ -168,8 +167,8 @@ public void testGetTemplateFailure() { @Test public void testGetTemplateAsync() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAsync().get(); @@ -193,8 +192,8 @@ public void testGetTemplateAsyncFailure() throws InterruptedException { @Test public void testGetTemplateAtVersionWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion("64"); @@ -216,8 +215,8 @@ public void testGetTemplateAtVersionWithStringValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync("55").get(); @@ -239,8 +238,8 @@ public void testGetTemplateAtVersionAsyncWithStringValueFailure() throws Interru @Test public void testGetTemplateAtVersionWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion(64L); @@ -262,8 +261,8 @@ public void testGetTemplateAtVersionWithLongValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync(55L).get(); @@ -431,8 +430,8 @@ public void testForcePublishTemplateAsyncFailure() throws InterruptedException { @Test public void testRollbackWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback("64"); @@ -454,8 +453,8 @@ public void testRollbackWithStringValueFailure() { @Test public void testRollbackAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync("55").get(); @@ -477,8 +476,8 @@ public void testRollbackAsyncWithStringValueFailure() throws InterruptedExceptio @Test public void testRollbackWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback(64L); @@ -500,8 +499,8 @@ public void testRollbackWithLongValueFailure() { @Test public void testRollbackAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate( - new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync(55L).get(); @@ -525,7 +524,8 @@ public void testRollbackAsyncWithLongValueFailure() throws InterruptedException @Test public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); @@ -548,7 +548,8 @@ public void testListVersionsWithNoOptionsFailure() { @Test public void testListVersionsAsyncWithNoOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); @@ -571,12 +572,13 @@ public void testListVersionsAsyncWithNoOptionsFailure() throws InterruptedExcept @Test public void testListVersionsWithOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersions( - ListVersionsOptions.builder().build()); + ListVersionsPage listVersionsPage = + remoteConfig.listVersions(ListVersionsOptions.builder().build()); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -595,12 +597,13 @@ public void testListVersionsWithOptionsFailure() { @Test public void testListVersionsAsyncWithOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync( - ListVersionsOptions.builder().build()).get(); + ListVersionsPage listVersionsPage = + remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); assertEquals("token", listVersionsPage.getNextPageToken()); } diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java index 1e7c0d470..3ac7f6b1c 100644 --- a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java +++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java @@ -26,9 +26,9 @@ public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{ private final ListVersionsResponse listVersionsResponse; private MockRemoteConfigClient(Template resultTemplate, - String resultServerTemplate, - ListVersionsResponse listVersionsResponse, - FirebaseRemoteConfigException exception) { + String resultServerTemplate, + ListVersionsResponse listVersionsResponse, + FirebaseRemoteConfigException exception) { this.resultTemplate = resultTemplate; this.resultServerTemplate = resultServerTemplate; this.listVersionsResponse = listVersionsResponse; @@ -102,4 +102,3 @@ public ListVersionsResponse listVersions( return listVersionsResponse; } } - diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java deleted file mode 100644 index 842fd808f..000000000 --- a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.assertNotEquals; - -import org.junit.Test; - -public class ParameterValueTest { - - @Test - public void testCreateExplicitValue() { - final ParameterValue.Explicit parameterValue = ParameterValue.of("title text"); - - assertEquals("title text", parameterValue.getValue()); - } - - @Test - public void testCreateInAppDefault() { - final ParameterValue.InAppDefault parameterValue = ParameterValue.inAppDefault(); - - assertEquals(ParameterValue.InAppDefault.class, parameterValue.getClass()); - } - - @Test - public void testEquality() { - ParameterValue.Explicit parameterValueOne = ParameterValue.of("value"); - ParameterValue.Explicit parameterValueTwo = ParameterValue.of("value"); - ParameterValue.Explicit parameterValueThree = ParameterValue.of("title"); - - assertEquals(parameterValueOne, parameterValueTwo); - assertNotEquals(parameterValueOne, parameterValueThree); - - ParameterValue.InAppDefault parameterValueFour = ParameterValue.inAppDefault(); - ParameterValue.InAppDefault parameterValueFive = ParameterValue.inAppDefault(); - - assertEquals(parameterValueFour, parameterValueFive); - } -} diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java new file mode 100644 index 000000000..2a5157953 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java @@ -0,0 +1,213 @@ +/* + * Copyright 2025 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.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.CustomSignalConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.ServerConditionResponse; +import java.util.ArrayList; +import org.junit.Test; + +/** Tests +* for {@link ServerCondition}. +* */ +public class ServerConditionTest { + + @Test + public void testConstructor() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition serverCondition = new ServerCondition("ios_en_1", conditions); + + assertEquals("ios_en_1", serverCondition.getName()); + assertEquals(conditions, serverCondition.getCondition()); + } + + @Test + public void testConstructorWithResponse() { + CustomSignalConditionResponse customResponse = + new CustomSignalConditionResponse() + .setKey("test_key") + .setOperator("NUMERIC_EQUAL") + .setTargetValues(ImmutableList.of("1")); + OneOfConditionResponse conditionResponse = + new OneOfConditionResponse().setCustomSignalCondition(customResponse); + ServerConditionResponse response = + new ServerConditionResponse().setName("ios_en_2").setServerCondition(conditionResponse); + ServerCondition serverCondition = new ServerCondition(response); + + assertEquals("ios_en_2", serverCondition.getName()); + assertEquals("test_key", serverCondition.getCondition().getCustomSignal().getCustomSignalKey()); + } + + @Test + public void testIllegalConstructor() { + IllegalArgumentException error = + assertThrows(IllegalArgumentException.class, () -> new ServerCondition(null, null)); + + assertEquals("condition name must not be null or empty", error.getMessage()); + } + + @Test + public void testConstructorWithNullServerConditionResponse() { + assertThrows(NullPointerException.class, () -> new ServerCondition(null)); + } + + @Test + public void testSetNullName() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition condition = new ServerCondition("ios", conditions); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> condition.setName(null)); + + assertEquals("condition name must not be null or empty", error.getMessage()); + } + + @Test + public void testSetEmptyName() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition condition = new ServerCondition("ios", conditions); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> condition.setName("")); + + assertEquals("condition name must not be null or empty", error.getMessage()); + } + + @Test + public void testSetNullServerCondition() { + OneOfCondition conditions = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + ServerCondition condition = new ServerCondition("ios", conditions); + + assertThrows(NullPointerException.class, () -> condition.setServerCondition(null)); + } + + @Test + public void testEquality() { + OneOfCondition conditionOne = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))))))))); + OneOfCondition conditionTwo = + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>(ImmutableList.of("100")))), + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_GREATER_THAN, + new ArrayList<>(ImmutableList.of("20")) + )))))))); + + final ServerCondition serverConditionOne = new ServerCondition("ios", conditionOne); + final ServerCondition serverConditionTwo = new ServerCondition("ios", conditionOne); + final ServerCondition serverConditionThree = new ServerCondition("android", conditionTwo); + final ServerCondition serverConditionFour = new ServerCondition("android", conditionTwo); + + assertEquals(serverConditionOne, serverConditionTwo); + assertEquals(serverConditionThree, serverConditionFour); + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java new file mode 100644 index 000000000..05a6b2812 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -0,0 +1,287 @@ +/* + * Copyright 2025 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.assertThrows; + +import com.google.api.core.ApiFuture; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.testing.TestUtils; +import org.junit.BeforeClass; +import org.junit.Test; + +/** +* Tests for {@link ServerTemplateImpl}. +*/ +public class ServerTemplateImplTest { + + private static final FirebaseOptions TEST_OPTIONS = + FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final String TEST_SERVER_TEMPLATE = + "{\n" + + " \"etag\": \"etag-123456789012-1\",\n" + + " \"parameters\": {},\n" + + " \"serverConditions\": [],\n" + + " \"parameterGroups\": {}\n" + + "}"; + + private static String cacheTemplate; + + @BeforeClass + public static void setUpClass() { + cacheTemplate = TestUtils.loadResource("getServerTemplateData.json"); + } + + @Test + public void testServerTemplateWithoutCacheValueThrowsException() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> new ServerTemplateImpl.Builder(null).defaultConfig(defaultConfig).build()); + + assertEquals("JSON String must not be null or empty.", error.getMessage()); + } + + @Test + public void testEvaluateCustomSignalReturnsDefaultValue() throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "100").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateCustomSignalReturnsConditionalValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateCustomSignalWithoutContextReturnsDefaultValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateCustomSignalWithInvalidContextReturnsDefaultValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "abc").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + + @Test + public void testEvaluateWithoutDefaultValueReturnsEmptyString() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("", evaluatedConfig.getString("Unset default value")); + } + + @Test + public void testEvaluateWithInvalidCacheValueThrowsException() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + String invalidJsonString = "abc"; + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(invalidJsonString) + .build(); + + FirebaseRemoteConfigException error = assertThrows(FirebaseRemoteConfigException.class, + () -> template.evaluate(context)); + + assertEquals("No Remote Config Server template in cache. Call load() before " + + "calling evaluate().", error.getMessage()); + } + + @Test + public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("", evaluatedConfig.getString("In-app default")); + } + + @Test + public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = + new KeysAndValues.Builder().put("users", "100").put("premium users", "20").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Chained conditions")); + } + + @Test + public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = + new KeysAndValues.Builder().put("users", "99").put("premium users", "30").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value", evaluatedConfig.getString("Chained conditions")); + } + + @Test + public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(ValueSource.STATIC, evaluatedConfig.getValueSource("invalid")); + } + + @Test + public void testGetEvaluateConfigInAppDefaultConfigReturnsDefaultValueSource() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().put("In-app default", "abc").build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(ValueSource.DEFAULT, evaluatedConfig.getValueSource("In-app default")); + } + + @Test + public void testGetEvaluateConfigUnsetDefaultConfigReturnsDefaultValueSource() throws Exception { + KeysAndValues defaultConfig = + new KeysAndValues.Builder().put("Unset default config", "abc").build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(ValueSource.DEFAULT, evaluatedConfig.getValueSource("Unset default config")); + } + + private static final String TEST_ETAG = "etag-123456789012-1"; + + private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "test-app"); + return new FirebaseRemoteConfig(app, client); + } + + @Test + public void testLoad() throws Exception { + KeysAndValues defaultConfig = + new KeysAndValues.Builder().put("Unset default config", "abc").build(); + + // Mock the HTTP client to return a predefined response + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + ServerTemplate template1 = + remoteConfig + .serverTemplateBuilder() + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + // Call the load method + ApiFuture loadFuture = template1.load(); + loadFuture.get(); + String cachedTemplate = template1.toJson(); + assertEquals(TEST_SERVER_TEMPLATE, cachedTemplate); + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ValueTest.java new file mode 100644 index 000000000..c1822b063 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ValueTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025 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.assertTrue; + +import org.junit.Test; + +public class ValueTest { + @Test + public void testGetSourceReturnsValueSource() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.getSource(), ValueSource.STATIC); + } + + @Test + public void testAsStringReturnsValueAsString() { + Value value = new Value(ValueSource.STATIC, "sample-string"); + assertEquals(value.asString(), "sample-string"); + } + + @Test + public void testAsStringReturnsDefaultEmptyString() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.asString(), ""); + } + + @Test + public void testAsLongReturnsDefaultValueForStaticSource() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.asLong(), 0L); + } + + @Test + public void testAsLongReturnsDefaultValueForInvalidSourceValue() { + Value value = new Value(ValueSource.REMOTE, "sample-string"); + assertEquals(value.asLong(), 0L); + } + + @Test + public void testAsLongReturnsSourceValueAsLong() { + Value value = new Value(ValueSource.REMOTE, "123"); + assertEquals(value.asLong(), 123L); + } + + @Test + public void testAsDoubleReturnsDefaultValueForStaticSource() { + Value value = new Value(ValueSource.STATIC); + assertEquals(value.asDouble(), 0, 0); + } + + @Test + public void testAsDoubleReturnsDefaultValueForInvalidSourceValue() { + Value value = new Value(ValueSource.REMOTE, "sample-string"); + assertEquals(value.asDouble(), 0, 0); + } + + @Test + public void testAsDoubleReturnsSourceValueAsDouble() { + Value value = new Value(ValueSource.REMOTE, "123.34"); + assertEquals(value.asDouble(), 123.34, 0); + } + + @Test + public void testAsBooleanReturnsDefaultValueForStaticSource() { + Value value = new Value(ValueSource.STATIC); + assertFalse(value.asBoolean()); + } + + @Test + public void testAsBooleanReturnsDefaultValueForInvalidSourceValue() { + Value value = new Value(ValueSource.REMOTE, "sample-string"); + assertFalse(value.asBoolean()); + } + + @Test + public void testAsBooleanReturnsSourceValueAsBoolean() { + Value value = new Value(ValueSource.REMOTE, "1"); + assertTrue(value.asBoolean()); + } + + @Test + public void testAsBooleanReturnsSourceValueYesAsBoolean() { + Value value = new Value(ValueSource.REMOTE, "YeS"); + assertTrue(value.asBoolean()); + } +} + \ No newline at end of file diff --git a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java index 515629660..1e524195f 100644 --- a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import com.google.firebase.remoteconfig.internal.TemplateResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; @@ -27,15 +28,16 @@ public class VersionTest { - @Test(expected = NullPointerException.class) + @Test public void testConstructorWithNullVersionResponse() { - new Version(null); + assertThrows(NullPointerException.class, () -> new Version(null)); } - @Test(expected = IllegalStateException.class) + @Test public void testConstructorWithInvalidUpdateTime() { - new Version(new VersionResponse() - .setUpdateTime("sunday,26th")); + assertThrows(IllegalStateException.class, () -> + new Version(new VersionResponse().setUpdateTime("sunday,26th"))); + } @Test diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json new file mode 100644 index 000000000..afc4c55d2 --- /dev/null +++ b/src/test/resources/getServerRemoteConfig.json @@ -0,0 +1,107 @@ +{ + "conditions": [ + { + "name": "custom_signal", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + } + ] + } + } + ] + } + } + }, + { + "name": "chained_conditions", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + }, + { + "customSignal": { + "customSignalOperator": "NUMERIC_GREATER_THAN", + "customSignalKey": "premium users", + "targetCustomSignalValues": [ + "20" + ] + } + } + ] + } + } + ] + } + } + } + ], + "parameters": { + "welcome_message_text": { + "defaultValue": { + "value": "welcome to app" + }, + "conditionalValues": { + "ios_en": { + "value": "welcome to app en" + } + }, + "description": "text for welcome message!", + "valueType": "STRING" + }, + "header_text": { + "defaultValue": { + "useInAppDefault": true + }, + "valueType": "STRING" + } + }, + "parameterGroups": { + "new menu": { + "description": "New Menu", + "parameters": { + "pumpkin_spice_season": { + "defaultValue": { + "value": "true" + }, + "description": "Whether it's currently pumpkin spice season.", + "valueType": "BOOLEAN" + } + } + } + }, + "version": { + "versionNumber": "17", + "updateOrigin": "ADMIN_SDK_NODE", + "updateType": "INCREMENTAL_UPDATE", + "updateUser": { + "email": "firebase-user@account.com", + "name": "dev-admin", + "imageUrl": "http://image.jpg" + }, + "updateTime": "2020-11-15T06:57:26.342763941Z", + "description": "promo config" + } +} diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json new file mode 100644 index 000000000..64731ef32 --- /dev/null +++ b/src/test/resources/getServerTemplateData.json @@ -0,0 +1,97 @@ +{ + "conditions": [ + { + "name": "custom_signal", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + } + ] + } + } + ] + } + } + }, + { + "name": "chained_conditions", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "customSignal": { + "customSignalOperator": "NUMERIC_LESS_THAN", + "customSignalKey": "users", + "targetCustomSignalValues": [ + "100" + ] + } + }, + { + "customSignal": { + "customSignalOperator": "NUMERIC_GREATER_THAN", + "customSignalKey": "premium users", + "targetCustomSignalValues": [ + "20" + ] + } + } + ] + } + } + ] + } + } + } + ], + "parameters": { + "Custom": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "custom_signal": { + "value": "Conditional value" + } + } + }, + "Welcome Message": { + "defaultValue": { + "value": "Welcome" + } + }, + "Unset default value": { + "defaultValue": { + "value": "" + } + }, + "Chained conditions": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "chained_conditions": { + "value": "Conditional value" + } + } + } + }, + "version": { + "versionNumber": "27", + "isLegacy": true + } +} From 6ad97cdb1f54b2d8150821e0cea90848b0e4ccd8 Mon Sep 17 00:00:00 2001 From: Athira M Date: Wed, 6 Aug 2025 06:46:24 +0530 Subject: [PATCH 03/29] Implement percent evaluation for server side RC (#1114) * [feat] Implement percent evaluation for server side RC --- .../remoteconfig/ConditionEvaluator.java | 67 +++++ .../remoteconfig/MicroPercentRange.java | 46 ++++ .../firebase/remoteconfig/OneOfCondition.java | 19 ++ .../remoteconfig/PercentCondition.java | 163 ++++++++++++ .../PercentConditionOperator.java | 55 ++++ .../remoteconfig/ConditionEvaluatorTest.java | 251 ++++++++++++++++++ .../FirebaseRemoteConfigClientImplTest.java | 54 ++-- .../remoteconfig/ServerConditionTest.java | 19 +- .../remoteconfig/ServerTemplateImplTest.java | 82 +++++- src/test/resources/getServerRemoteConfig.json | 35 +++ src/test/resources/getServerTemplateData.json | 76 ++++++ 11 files changed, 843 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/PercentCondition.java create mode 100644 src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java index e333ca2b5..2bd828e2c 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -23,6 +23,10 @@ import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -38,6 +42,7 @@ final class ConditionEvaluator { private static final int MAX_CONDITION_RECURSION_DEPTH = 10; private static final Logger logger = LoggerFactory.getLogger(ConditionEvaluator.class); + private static final BigInteger MICRO_PERCENT_MODULO = BigInteger.valueOf(100_000_000L); /** * Evaluates server conditions and assigns a boolean value to each condition. @@ -83,6 +88,8 @@ private boolean evaluateCondition(OneOfCondition condition, KeysAndValues contex return false; } else if (condition.getCustomSignal() != null) { return evaluateCustomSignalCondition(condition.getCustomSignal(), context); + } else if (condition.getPercent() != null) { + return evaluatePercentCondition(condition.getPercent(), context); } logger.atWarn().log("Received invalid condition for evaluation."); return false; @@ -179,6 +186,66 @@ private boolean evaluateCustomSignalCondition(CustomSignalCondition condition, } } + private boolean evaluatePercentCondition(PercentCondition condition, + KeysAndValues context) { + if (!context.containsKey("randomizationId")) { + logger.warn("Percentage operation must not be performed without randomizationId"); + return false; + } + + PercentConditionOperator operator = condition.getPercentConditionOperator(); + + // The micro-percent interval to be used with the BETWEEN operator. + MicroPercentRange microPercentRange = condition.getMicroPercentRange(); + int microPercentUpperBound = microPercentRange != null + ? microPercentRange.getMicroPercentUpperBound() + : 0; + int microPercentLowerBound = microPercentRange != null + ? microPercentRange.getMicroPercentLowerBound() + : 0; + // The limit of percentiles to target in micro-percents when using the + // LESS_OR_EQUAL and GREATER_THAN operators. The value must be in the range [0 + // and 100000000]. + int microPercent = condition.getMicroPercent(); + BigInteger microPercentile = getMicroPercentile(condition.getSeed(), + context.get("randomizationId")); + switch (operator) { + case LESS_OR_EQUAL: + return microPercentile.compareTo(BigInteger.valueOf(microPercent)) <= 0; + case GREATER_THAN: + return microPercentile.compareTo(BigInteger.valueOf(microPercent)) > 0; + case BETWEEN: + return microPercentile.compareTo(BigInteger.valueOf(microPercentLowerBound)) > 0 + && microPercentile.compareTo(BigInteger.valueOf(microPercentUpperBound)) <= 0; + case UNSPECIFIED: + default: + return false; + } + } + + private BigInteger getMicroPercentile(String seed, String randomizationId) { + String seedPrefix = seed != null && !seed.isEmpty() ? seed + "." : ""; + String stringToHash = seedPrefix + randomizationId; + BigInteger hash = hashSeededRandomizationId(stringToHash); + BigInteger microPercentile = hash.mod(MICRO_PERCENT_MODULO); + + return microPercentile; + } + + private BigInteger hashSeededRandomizationId(String seededRandomizationId) { + try { + // Create a SHA-256 hash. + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(seededRandomizationId.getBytes(StandardCharsets.UTF_8)); + + // Convert the hash bytes to a BigInteger + return new BigInteger(1, hashBytes); + } catch (NoSuchAlgorithmException e) { + logger.error("SHA-256 algorithm not found", e); + throw new RuntimeException("SHA-256 algorithm not found", e); + } + } + private boolean compareStrings(ImmutableList targetValues, String customSignal, BiPredicate compareFunction) { return targetValues.stream().anyMatch(targetValue -> diff --git a/src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java b/src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java new file mode 100644 index 000000000..abd5711c6 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/MicroPercentRange.java @@ -0,0 +1,46 @@ +/* +* Copyright 2025 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 com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.MicroPercentRangeResponse; + +class MicroPercentRange { + private final int microPercentLowerBound; + private final int microPercentUpperBound; + + public MicroPercentRange(@Nullable Integer microPercentLowerBound, + @Nullable Integer microPercentUpperBound) { + this.microPercentLowerBound = microPercentLowerBound != null ? microPercentLowerBound : 0; + this.microPercentUpperBound = microPercentUpperBound != null ? microPercentUpperBound : 0; + } + + int getMicroPercentLowerBound() { + return microPercentLowerBound; + } + + int getMicroPercentUpperBound() { + return microPercentUpperBound; + } + + MicroPercentRangeResponse toMicroPercentRangeResponse() { + MicroPercentRangeResponse microPercentRangeResponse = new MicroPercentRangeResponse(); + microPercentRangeResponse.setMicroPercentLowerBound(this.microPercentLowerBound); + microPercentRangeResponse.setMicroPercentUpperBound(this.microPercentUpperBound); + return microPercentRangeResponse; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java index 7e352ec0d..be3f5fd3e 100644 --- a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java +++ b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java @@ -26,6 +26,7 @@ class OneOfCondition { private OrCondition orCondition; private AndCondition andCondition; + private PercentCondition percent; private CustomSignalCondition customSignal; private String trueValue; private String falseValue; @@ -37,6 +38,9 @@ class OneOfCondition { if (oneOfconditionResponse.getAndCondition() != null) { this.andCondition = new AndCondition(oneOfconditionResponse.getAndCondition()); } + if (oneOfconditionResponse.getPercentCondition() != null) { + this.percent = new PercentCondition(oneOfconditionResponse.getPercentCondition()); + } if (oneOfconditionResponse.getCustomSignalCondition() != null) { this.customSignal = new CustomSignalCondition(oneOfconditionResponse.getCustomSignalCondition()); @@ -47,6 +51,7 @@ class OneOfCondition { OneOfCondition() { this.orCondition = null; this.andCondition = null; + this.percent = null; this.trueValue = null; this.falseValue = null; } @@ -71,6 +76,11 @@ String isFalse() { return falseValue; } + @Nullable + PercentCondition getPercent() { + return percent; + } + @Nullable CustomSignalCondition getCustomSignal() { return customSignal; @@ -88,6 +98,12 @@ OneOfCondition setAndCondition(@NonNull AndCondition andCondition) { return this; } + OneOfCondition setPercent(@NonNull PercentCondition percent) { + checkNotNull(percent, "`Percent` condition cannot be set to null."); + this.percent = percent; + return this; + } + OneOfCondition setCustomSignal(@NonNull CustomSignalCondition customSignal) { checkNotNull(customSignal, "`Custom signal` condition cannot be set to null."); this.customSignal = customSignal; @@ -115,6 +131,9 @@ OneOfConditionResponse toOneOfConditionResponse() { if (this.customSignal != null) { oneOfConditionResponse.setCustomSignalCondition(this.customSignal.toCustomConditonResponse()); } + if (this.percent != null) { + oneOfConditionResponse.setPercentCondition(this.percent.toPercentConditionResponse()); + } return oneOfConditionResponse; } } diff --git a/src/main/java/com/google/firebase/remoteconfig/PercentCondition.java b/src/main/java/com/google/firebase/remoteconfig/PercentCondition.java new file mode 100644 index 000000000..c5763200b --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/PercentCondition.java @@ -0,0 +1,163 @@ +/* +* Copyright 2025 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.PercentConditionResponse; + +/** Represents a condition that compares the instance pseudo-random percentile to a given limit. */ +public final class PercentCondition { + private int microPercent; + private MicroPercentRange microPercentRange; + private final PercentConditionOperator percentConditionOperator; + private final String seed; + + /** + * Create a percent condition for operator BETWEEN. + * + * @param microPercent The limit of percentiles to target in micro-percents when using the + * LESS_OR_EQUAL and GREATER_THAN operators. The value must be in the range [0 and 100000000]. + * @param percentConditionOperator The choice of percent operator to determine how to compare + * targets to percent(s). + * @param seed The seed used when evaluating the hash function to map an instance to a value in + * the hash space. This is a string which can have 0 - 32 characters and can contain ASCII + * characters [-_.0-9a-zA-Z].The string is case-sensitive. + */ + PercentCondition( + @Nullable Integer microPercent, + @NonNull PercentConditionOperator percentConditionOperator, + @NonNull String seed) { + checkNotNull(percentConditionOperator, "Percentage operator must not be null."); + checkArgument(!Strings.isNullOrEmpty(seed), "Seed must not be null or empty."); + this.microPercent = microPercent != null ? microPercent : 0; + this.percentConditionOperator = percentConditionOperator; + this.seed = seed; + } + + /** + * Create a percent condition for operators GREATER_THAN and LESS_OR_EQUAL. + * + * @param microPercentRange The micro-percent interval to be used with the BETWEEN operator. + * @param percentConditionOperator The choice of percent operator to determine how to compare + * targets to percent(s). + * @param seed The seed used when evaluating the hash function to map an instance to a value in + * the hash space. This is a string which can have 0 - 32 characters and can contain ASCII + * characters [-_.0-9a-zA-Z].The string is case-sensitive. + */ + PercentCondition( + @NonNull MicroPercentRange microPercentRange, + @NonNull PercentConditionOperator percentConditionOperator, + String seed) { + checkNotNull(microPercentRange, "Percent range must not be null."); + checkNotNull(percentConditionOperator, "Percentage operator must not be null."); + this.microPercentRange = microPercentRange; + this.percentConditionOperator = percentConditionOperator; + this.seed = seed; + } + + /** + * Creates a new {@link PercentCondition} from API response. + * + * @param percentCondition the conditions obtained from server call. + */ + PercentCondition(PercentConditionResponse percentCondition) { + checkArgument( + !Strings.isNullOrEmpty(percentCondition.getSeed()), "Seed must not be empty or null"); + this.microPercent = percentCondition.getMicroPercent(); + this.seed = percentCondition.getSeed(); + switch (percentCondition.getPercentOperator()) { + case "BETWEEN": + this.percentConditionOperator = PercentConditionOperator.BETWEEN; + break; + case "GREATER_THAN": + this.percentConditionOperator = PercentConditionOperator.GREATER_THAN; + break; + case "LESS_OR_EQUAL": + this.percentConditionOperator = PercentConditionOperator.LESS_OR_EQUAL; + break; + default: + this.percentConditionOperator = PercentConditionOperator.UNSPECIFIED; + } + checkArgument( + this.percentConditionOperator != PercentConditionOperator.UNSPECIFIED, + "Percentage operator is invalid"); + if (percentCondition.getMicroPercentRange() != null) { + this.microPercentRange = + new MicroPercentRange( + percentCondition.getMicroPercentRange().getMicroPercentLowerBound(), + percentCondition.getMicroPercentRange().getMicroPercentUpperBound()); + } + } + + /** + * Gets the limit of percentiles to target in micro-percents when using the LESS_OR_EQUAL and + * GREATER_THAN operators. The value must be in the range [0 and 100000000]. + * + * @return micro percent. + */ + @Nullable + public int getMicroPercent() { + return microPercent; + } + + /** + * Gets micro-percent interval to be used with the BETWEEN operator. + * + * @return micro percent range. + */ + @Nullable + public MicroPercentRange getMicroPercentRange() { + return microPercentRange; + } + + /** + * Gets choice of percent operator to determine how to compare targets to percent(s). + * + * @return operator. + */ + @NonNull + public PercentConditionOperator getPercentConditionOperator() { + return percentConditionOperator; + } + + /** + * The seed used when evaluating the hash function to map an instance to a value in the hash + * space. This is a string which can have 0 - 32 characters and can contain ASCII characters + * [-_.0-9a-zA-Z].The string is case-sensitive. + * + * @return seed. + */ + @NonNull + public String getSeed() { + return seed; + } + + PercentConditionResponse toPercentConditionResponse() { + PercentConditionResponse percentConditionResponse = new PercentConditionResponse(); + percentConditionResponse.setMicroPercent(this.microPercent); + percentConditionResponse.setMicroPercentRange( + this.microPercentRange.toMicroPercentRangeResponse()); + percentConditionResponse.setPercentOperator(this.percentConditionOperator.getOperator()); + percentConditionResponse.setSeed(this.seed); + return percentConditionResponse; + } +} diff --git a/src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java b/src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java new file mode 100644 index 000000000..478f13e4e --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/PercentConditionOperator.java @@ -0,0 +1,55 @@ +/* +* Copyright 2025 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 com.google.firebase.internal.NonNull; + +/** +* Defines supported operators for percent conditions. +*/ +public enum PercentConditionOperator { + BETWEEN("BETWEEN"), + GREATER_THAN("GREATER_THAN"), + LESS_OR_EQUAL("LESS_OR_EQUAL"), + UNSPECIFIED("PERCENT_OPERATOR_UNSPECIFIED"); + + private final String operator; + + /** + * Creates percent condition operator. + * + * @param operator The choice of percent operator to determine how to compare targets to + * percent(s). + */ + PercentConditionOperator(@NonNull String operator) { + checkArgument(!Strings.isNullOrEmpty(operator), "Operator must not be null or empty."); + this.operator = operator; + } + + /** + * Gets percent condition operator. + * + * @return operator. + */ + @NonNull + public String getOperator() { + return operator; + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java index e2277c7e4..0cd6e4528 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ConditionEvaluatorTest.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.Map; +import java.util.UUID; import org.junit.Test; @@ -86,6 +87,220 @@ public void testEvaluateConditionsNonOrTopConditionToTrue() { assertTrue(result.get("is_enabled")); } + @Test + public void testEvaluateConditionsPercentConditionWithInvalidOperatorToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(0, + PercentConditionOperator.UNSPECIFIED, "seed"); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionPercent); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "abc"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsPercentConditionLessOrEqualMaxToTrue() { + OneOfCondition oneOfConditionPercent = createPercentCondition(10_000_0000, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsPercentConditionLessOrEqualMinToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(0, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsPercentConditionUndefinedMicroPercentToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(null, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsUseZeroForUndefinedPercentRange() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(null, null, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsUseZeroForUndefinedUpperBound() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(0, null, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsUseZeroForUndefinedLowerBound() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(null, 10_000_0000, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsGreaterThanMinToTrue() { + OneOfCondition oneOfConditionPercent = createPercentCondition(0, + PercentConditionOperator.GREATER_THAN, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsGreaterThanMaxToFalse() { + OneOfCondition oneOfConditionPercent = createPercentCondition(10_000_0000, + PercentConditionOperator.GREATER_THAN, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsBetweenMinAndMaxToTrue() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(0, 10_000_0000, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertTrue(result.get("is_enabled")); + } + + @Test + public void testEvaluatedConditionsBetweenEqualBoundsToFalse() { + OneOfCondition oneOfConditionPercent = createBetweenPercentCondition(5_000_000, + 5_000_000, "seed"); + OneOfCondition oneOfConditionAnd = createOneOfAndCondition(oneOfConditionPercent); + OneOfCondition oneOfConditionOr = createOneOfOrCondition(oneOfConditionAnd); + ServerCondition condition = new ServerCondition("is_enabled", oneOfConditionOr); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", "123"); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + assertFalse(result.get("is_enabled")); + } + + @Test + public void testEvaluateConditionsLessOrEqualToApprox() { + OneOfCondition oneOfConditionPerCondition = createPercentCondition(10_000_000, + PercentConditionOperator.LESS_OR_EQUAL, "seed"); + // 284 is 3 standard deviations for 100k trials with 10% probability. + int tolerance = 284; + + int truthyAssignments = evaluateRandomAssignments(oneOfConditionPerCondition, 100000); + + // Evaluate less than or equal 10% to approx 10% + assertTrue(truthyAssignments >= 10_000 - tolerance); + assertTrue(truthyAssignments <= 10_000 + tolerance); + } + + @Test + public void testEvaluateConditionsBetweenApproximateToTrue() { + // Micropercent range is 40% to 60%. + OneOfCondition oneOfConditionPerCondition = createBetweenPercentCondition(40_000_000, + 60_000_000, "seed"); + // 379 is 3 standard deviations for 100k trials with 20% probability. + int tolerance = 379; + + int truthyAssignments = evaluateRandomAssignments(oneOfConditionPerCondition, 100000); + + // Evaluate between 40% to 60% to approx 20% + assertTrue(truthyAssignments >= 20_000 - tolerance); + assertTrue(truthyAssignments <= 20_000 + tolerance); + } + + @Test + public void testEvaluateConditionsInterquartileToFiftyPercent() { + // Micropercent range is 25% to 75%. + OneOfCondition oneOfConditionPerCondition = createBetweenPercentCondition(25_000_000, + 75_000_000, "seed"); + // 474 is 3 standard deviations for 100k trials with 50% probability. + int tolerance = 474; + + int truthyAssignments = evaluateRandomAssignments(oneOfConditionPerCondition, 100000); + + // Evaluate between 25% to 75 to approx 50% + assertTrue(truthyAssignments >= 50_000 - tolerance); + assertTrue(truthyAssignments <= 50_000 + tolerance); + } + @Test public void testEvaluateConditionsCustomSignalNumericLessThanToFalse() { ServerCondition condition = createCustomSignalServerCondition( @@ -551,6 +766,42 @@ private ServerCondition createCustomSignalServerCondition( return new ServerCondition("signal_key", oneOfConditionCustomSignal); } + private int evaluateRandomAssignments(OneOfCondition percentCondition, int numOfAssignments) { + int evalTrueCount = 0; + ServerCondition condition = new ServerCondition("is_enabled", percentCondition); + for (int i = 0; i < numOfAssignments; i++) { + UUID randomizationId = UUID.randomUUID(); + KeysAndValues.Builder contextBuilder = new KeysAndValues.Builder(); + contextBuilder.put("randomizationId", randomizationId.toString()); + + Map result = conditionEvaluator.evaluateConditions(Arrays.asList(condition), + contextBuilder.build()); + + if (result.get("is_enabled")) { + evalTrueCount++; + } + } + return evalTrueCount; + } + + private OneOfCondition createPercentCondition(Integer microPercent, + PercentConditionOperator operator, String seed) { + PercentCondition percentCondition = new PercentCondition(microPercent, operator, seed); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setPercent(percentCondition); + return oneOfCondition; + } + + private OneOfCondition createBetweenPercentCondition(Integer lowerBound, Integer upperBound, + String seed) { + MicroPercentRange microPercentRange = new MicroPercentRange(lowerBound, upperBound); + PercentCondition percentCondition = new PercentCondition(microPercentRange, + PercentConditionOperator.BETWEEN, seed); + OneOfCondition oneOfCondition = new OneOfCondition(); + oneOfCondition.setPercent(percentCondition); + return oneOfCondition; + } + private OneOfCondition createOneOfOrCondition(OneOfCondition condition) { OrCondition orCondition = condition != null ? new OrCondition(ImmutableList.of(condition)) : new OrCondition(ImmutableList.of()); diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index e92526555..d820dda76 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -138,6 +138,23 @@ public class FirebaseRemoteConfigClientImplTest { .NUMERIC_LESS_THAN, new ArrayList<>( ImmutableList.of("100"))))))))))), + new ServerCondition("percent", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange( + 12000000, 100000000), + PercentConditionOperator.BETWEEN, + "3maarirs9xzs"))))))))), new ServerCondition("chained_conditions", null) .setServerCondition( new OneOfCondition() @@ -149,22 +166,29 @@ public class FirebaseRemoteConfigClientImplTest { new AndCondition( ImmutableList.of( new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( - "users", - CustomSignalOperator - .NUMERIC_LESS_THAN, - new ArrayList<>( - ImmutableList.of("100")))), + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))), + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "premium users", + CustomSignalOperator + .NUMERIC_GREATER_THAN, + new ArrayList<>( + ImmutableList.of("20")))), new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( - "premium users", - CustomSignalOperator - .NUMERIC_GREATER_THAN, - new ArrayList<>( - ImmutableList.of("20")))) - )))))))); + .setPercent( + new PercentCondition( + new MicroPercentRange( + 25000000, 100000000), + PercentConditionOperator.BETWEEN, + "cla24qoibb61")) + )))))))); private static final Version EXPECTED_VERSION = new Version( diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java index 2a5157953..6cbece1c0 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerConditionTest.java @@ -193,14 +193,19 @@ public void testEquality() { "users", CustomSignalOperator.NUMERIC_LESS_THAN, new ArrayList<>(ImmutableList.of("100")))), - new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( "users", - CustomSignalOperator - .NUMERIC_GREATER_THAN, - new ArrayList<>(ImmutableList.of("20")) - )))))))); + CustomSignalOperator + .NUMERIC_GREATER_THAN, + new ArrayList<>(ImmutableList.of("20")))), + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange(25000000, 100000000), + PercentConditionOperator.BETWEEN, + "cla24qoibb61")))))))); final ServerCondition serverConditionOne = new ServerCondition("ios", conditionOne); final ServerCondition serverConditionTwo = new ServerCondition("ios", conditionOne); diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java index 05a6b2812..ae70d2366 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -126,6 +126,37 @@ public void testEvaluateCustomSignalWithInvalidContextReturnsDefaultValue() assertEquals("Default value", evaluatedConfig.getString("Custom")); } + @Test + public void testEvaluatePercentWithoutRandomizationIdReturnsDefaultValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Percent")); + } + + @Test + public void testEvaluatePercentReturnsConditionalValue() throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value", evaluatedConfig.getString("Percent")); + } + @Test public void testEvaluateWithoutDefaultValueReturnsEmptyString() throws FirebaseRemoteConfigException { @@ -176,11 +207,43 @@ public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception { assertEquals("", evaluatedConfig.getString("In-app default")); } + @Test + public void testEvaluateWithDerivedInAppDefaultReturnsDefaultValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Default value", evaluatedConfig.getString("Derived in-app default")); + } + + @Test + public void testEvaluateWithMultipleConditionReturnsConditionalValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals("Conditional value 1", evaluatedConfig.getString("Multiple conditions")); + } + @Test public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - KeysAndValues context = - new KeysAndValues.Builder().put("users", "100").put("premium users", "20").build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "100") + .put("premium users", 20) + .put("randomizationId", "user") + .build(); ServerTemplate template = new ServerTemplateImpl.Builder(null) .defaultConfig(defaultConfig) @@ -208,6 +271,21 @@ public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws assertEquals("Conditional value", evaluatedConfig.getString("Chained conditions")); } + @Test + public void testGetEvaluateConfigOnInvalidTypeReturnsDefaultValue() throws Exception { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(context); + + assertEquals(0L, evaluatedConfig.getLong("Percent")); + } + @Test public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json index afc4c55d2..2e3c35566 100644 --- a/src/test/resources/getServerRemoteConfig.json +++ b/src/test/resources/getServerRemoteConfig.json @@ -24,6 +24,31 @@ } } }, + { + "name": "percent", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "percent": { + "percentOperator": "BETWEEN", + "seed": "3maarirs9xzs", + "microPercentRange": { + "microPercentLowerBound": 12000000, + "microPercentUpperBound": 100000000 + } + } + } + ] + } + } + ] + } + } + }, { "name": "chained_conditions", "condition": { @@ -49,6 +74,16 @@ "20" ] } + }, + { + "percent": { + "percentOperator": "BETWEEN", + "seed": "cla24qoibb61", + "microPercentRange": { + "microPercentLowerBound": 25000000, + "microPercentUpperBound": 100000000 + } + } } ] } diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json index 64731ef32..27e3507d6 100644 --- a/src/test/resources/getServerTemplateData.json +++ b/src/test/resources/getServerTemplateData.json @@ -24,6 +24,31 @@ } } }, + { + "name": "percent", + "condition": { + "orCondition": { + "conditions": [ + { + "andCondition": { + "conditions": [ + { + "percent": { + "percentOperator": "BETWEEN", + "seed": "3maarirs9xzs", + "microPercentRange": { + "microPercentLowerBound": 12000000, + "microPercentUpperBound": 100000000 + } + } + } + ] + } + } + ] + } + } + }, { "name": "chained_conditions", "condition": { @@ -48,6 +73,14 @@ "targetCustomSignalValues": [ "20" ] + }, + "percent": { + "percentOperator": "BETWEEN", + "seed": "cla24qoibb61", + "microPercentRange": { + "microPercentLowerBound": 25000000, + "microPercentUpperBound": 100000000 + } } } ] @@ -59,6 +92,16 @@ } ], "parameters": { + "Percent": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "percent": { + "value": "Conditional value" + } + } + }, "Custom": { "defaultValue": { "value": "Default value" @@ -79,6 +122,39 @@ "value": "" } }, + "In-app default": { + "defaultValue": { + "useInAppDefault": true + }, + "conditionalValues": { + "percent": { + "value": "Conditional value" + } + } + }, + "Derived in-app default": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "percent": { + "useInAppDefault": true + } + } + }, + "Multiple conditions": { + "defaultValue": { + "value": "Default value" + }, + "conditionalValues": { + "custom_signal": { + "value": "Conditional value 1" + }, + "percent": { + "value": "Conditional value 2" + } + } + }, "Chained conditions": { "defaultValue": { "value": "Default value" From 486e84138b6d79e686627c1076226bb1cf58bfaa Mon Sep 17 00:00:00 2001 From: Athira M Date: Thu, 7 Aug 2025 22:48:22 +0530 Subject: [PATCH 04/29] Handle empty context --- .../remoteconfig/ServerTemplateImpl.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java index c85fa160b..6d3d675d7 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -20,6 +20,7 @@ import com.google.api.core.ApiFutures; import com.google.common.collect.ImmutableMap; import com.google.firebase.ErrorCode; +import com.google.firebase.internal.Nullable; import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -78,10 +79,11 @@ private ServerTemplateImpl(Builder builder) { } @Override - public ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigException { + public ServerConfig evaluate(@Nullable KeysAndValues context) + throws FirebaseRemoteConfigException { if (this.cache == null) { throw new FirebaseRemoteConfigException(ErrorCode.FAILED_PRECONDITION, - "No Remote Config Server template in cache. Call load() before calling evaluate()."); + "No Remote Config Server template in cache. Call load() before calling evaluate()."); } Map configValues = new HashMap<>(); @@ -93,8 +95,11 @@ public ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigE } ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); - ImmutableMap evaluatedCondition = ImmutableMap.copyOf( - conditionEvaluator.evaluateConditions(cache.getServerConditions(), context)); + ImmutableMap evaluatedCondition = + context != null && !cache.getServerConditions().isEmpty() + ? ImmutableMap.copyOf( + conditionEvaluator.evaluateConditions(cache.getServerConditions(), context)) + : ImmutableMap.of(); ImmutableMap parameters = ImmutableMap.copyOf(cache.getParameters()); mergeDerivedConfigValues(evaluatedCondition, parameters, configValues); @@ -103,8 +108,7 @@ public ServerConfig evaluate(KeysAndValues context) throws FirebaseRemoteConfigE @Override public ServerConfig evaluate() throws FirebaseRemoteConfigException { - KeysAndValues context = new KeysAndValues.Builder().build(); - return evaluate(context); + return evaluate(null); } @Override From 91acd60ca8fb06316b53e1270fa4af0467967994 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Fri, 8 Aug 2025 17:19:09 +0530 Subject: [PATCH 05/29] fix issue related to update time --- .../firebase/remoteconfig/ServerVersion.java | 212 ++++++++++++++++++ .../remoteconfig/ServerVersionTest.java | 99 ++++++++ 2 files changed, 311 insertions(+) create mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerVersion.java create mode 100644 src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java b/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java new file mode 100644 index 000000000..9db70a75e --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java @@ -0,0 +1,212 @@ +/* + * 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.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.TemplateResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; + +import java.util.Objects; + +/** + * Represents a Server Remote Config template version. + * Output only, except for the version description. Contains metadata about a particular + * version of the Remote Config template. All fields are set at the time the specified Remote + * Config template is published. A version's description field may be specified when + * publishing a template. + */ +public final class ServerVersion { + + private final String versionNumber; + private final String updateTime; + private final String updateOrigin; + private final String updateType; + private final User updateUser; + private final String rollbackSource; + private final boolean legacy; + private String description; + + private ServerVersion() { + this.versionNumber = null; + this.updateTime = null; + this.updateOrigin = null; + this.updateType = null; + this.updateUser = null; + this.rollbackSource = null; + this.legacy = false; + } + + ServerVersion(@NonNull VersionResponse versionResponse) { + checkNotNull(versionResponse); + this.versionNumber = versionResponse.getVersionNumber(); + this.updateTime = versionResponse.getUpdateTime(); + this.updateOrigin = versionResponse.getUpdateOrigin(); + this.updateType = versionResponse.getUpdateType(); + TemplateResponse.UserResponse userResponse = versionResponse.getUpdateUser(); + this.updateUser = (userResponse != null) ? new User(userResponse) : null; + this.description = versionResponse.getDescription(); + this.rollbackSource = versionResponse.getRollbackSource(); + this.legacy = versionResponse.isLegacy(); + } + + /** + * Creates a new {@link Version} with a description. + */ + public static ServerVersion withDescription(String description) { + return new ServerVersion().setDescription(description); + } + + /** + * Gets the version number of the template. + * + * @return The version number or null. + */ + @Nullable + public String getVersionNumber() { + return versionNumber; + } + + /** + * Gets the update time of the version. The timestamp of when this version of the Remote Config + * template was written to the Remote Config backend. + * + * @return The update time of the version or null. + */ + @Nullable + public String getUpdateTime() { + return updateTime; + } + + /** + * Gets the origin of the template update action. + * + * @return The origin of the template update action or null. + */ + @Nullable + public String getUpdateOrigin() { + return updateOrigin; + } + + /** + * Gets the type of the template update action. + * + * @return The type of the template update action or null. + */ + @Nullable + public String getUpdateType() { + return updateType; + } + + /** + * Gets the update user of the template. + * An aggregation of all metadata fields about the account that performed the update. + * + * @return The update user of the template or null. + */ + @Nullable + public User getUpdateUser() { + return updateUser; + } + + /** + * Gets the user-provided description of the corresponding Remote Config template. + * + * @return The description of the template or null. + */ + @Nullable + public String getDescription() { + return description; + } + + /** + * Gets the rollback source of the template. + * + *

The serverVersion number of the Remote Config template that has become the current serverVersion + * due to a rollback. Only present if this serverVersion is the result of a rollback. + * + * @return The rollback source of the template or null. + */ + @Nullable + public String getRollbackSource() { + return rollbackSource; + } + + /** + * Indicates whether this Remote Config template was published before serverVersion history was + * supported. + * + * @return true if the template was published before serverVersion history was supported, + * and false otherwise. + */ + public boolean isLegacy() { + return legacy; + } + + /** + * Sets the user-provided description of the template. + * + * @param description The description of the template. + * @return This {@link ServerVersion}. + */ + public ServerVersion setDescription(String description) { + this.description = description; + return this; + } + + VersionResponse toVersionResponse(boolean includeAll) { + VersionResponse versionResponse = new VersionResponse().setDescription(this.description); + if (includeAll) { + versionResponse.setUpdateTime(this.updateTime) + .setLegacy(this.legacy) + .setRollbackSource(this.rollbackSource) + .setUpdateOrigin(this.updateOrigin) + .setUpdateType(this.updateType) + .setUpdateUser((this.updateUser == null) ? null : this.updateUser.toUserResponse()) + .setVersionNumber(this.versionNumber); + } + return versionResponse; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerVersion serverVersion = (ServerVersion) o; + return updateTime == serverVersion.updateTime + && legacy == serverVersion.legacy + && Objects.equals(versionNumber, serverVersion.versionNumber) + && Objects.equals(updateOrigin, serverVersion.updateOrigin) + && Objects.equals(updateType, serverVersion.updateType) + && Objects.equals(updateUser, serverVersion.updateUser) + && Objects.equals(description, serverVersion.description) + && Objects.equals(rollbackSource, serverVersion.rollbackSource); + } + + @Override + public int hashCode() { + return Objects + .hash(versionNumber, updateTime, updateOrigin, updateType, updateUser, description, + rollbackSource, legacy); + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java new file mode 100644 index 000000000..a1ddd0a73 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java @@ -0,0 +1,99 @@ +/* + * 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.assertNotEquals; +import static org.junit.Assert.assertNull; + +import com.google.firebase.remoteconfig.internal.TemplateResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; +import org.junit.Test; + +public class ServerVersionTest { + + @Test(expected = NullPointerException.class) + public void testConstructorWithNullServerVersionResponse() { + new ServerVersion(null); + } + + @Test + public void testConstructorWithValidZuluUpdateTime() { + ServerVersion serverVersion = new ServerVersion(new VersionResponse() + .setUpdateTime("2020-12-08T15:49:51.887878Z")); + assertEquals("2020-12-08T15:49:51.887878Z", serverVersion.getUpdateTime()); + } + + @Test + public void testConstructorWithValidUTCUpdateTime() { + ServerVersion serverVersion = new ServerVersion(new VersionResponse() + .setUpdateTime("Tue, 08 Dec 2020 15:49:51 GMT")); + assertEquals("Tue, 08 Dec 2020 15:49:51 GMT", serverVersion.getUpdateTime()); + } + + @Test + public void testWithDescription() { + final ServerVersion serverVersion = ServerVersion.withDescription("version description text"); + + assertEquals("version description text", serverVersion.getDescription()); + assertNull(serverVersion.getVersionNumber()); + assertEquals(null, serverVersion.getUpdateTime()); + assertNull(serverVersion.getUpdateOrigin()); + assertNull(serverVersion.getUpdateType()); + assertNull(serverVersion.getUpdateUser()); + assertNull(serverVersion.getRollbackSource()); + assertFalse(serverVersion.isLegacy()); + } + + @Test + public void testEquality() { + final ServerVersion versionOne = new ServerVersion(new VersionResponse()); + final ServerVersion versionTwo = new ServerVersion(new VersionResponse()); + + assertEquals(versionOne, versionTwo); + + final ServerVersion versionThree = ServerVersion.withDescription("abcd"); + final ServerVersion versionFour = ServerVersion.withDescription("abcd"); + final ServerVersion versionFive = new ServerVersion(new VersionResponse()).setDescription("abcd"); + + assertEquals(versionThree, versionFour); + assertEquals(versionThree, versionFive); + + final ServerVersion versionSix = ServerVersion.withDescription("efgh"); + + assertNotEquals(versionThree, versionSix); + assertNotEquals(versionOne, versionSix); + + final VersionResponse versionResponse = new VersionResponse() + .setVersionNumber("23") + .setUpdateTime("2014-10-02T15:01:23.045123456Z") + .setUpdateOrigin("ADMIN_SDK") + .setUpdateUser(new TemplateResponse.UserResponse() + .setEmail("user@email.com") + .setName("user-1234") + .setImageUrl("http://user.jpg")) + .setUpdateType("INCREMENTAL_UPDATE"); + final ServerVersion versionSeven = new ServerVersion(versionResponse); + final ServerVersion versionEight = new ServerVersion(versionResponse); + + assertEquals(versionSeven, versionEight); + assertNotEquals(versionOne, versionSeven); + assertNotEquals(versionThree, versionSeven); + assertNotEquals(versionSix, versionSeven); + } +} From 422d227cf51072129817217201167fc667d6ca54 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Fri, 8 Aug 2025 17:55:05 +0530 Subject: [PATCH 06/29] fix string equality --- .../java/com/google/firebase/remoteconfig/ServerVersion.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java b/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java index 9db70a75e..5475c2f98 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java @@ -193,8 +193,8 @@ public boolean equals(Object o) { return false; } ServerVersion serverVersion = (ServerVersion) o; - return updateTime == serverVersion.updateTime - && legacy == serverVersion.legacy + return legacy == serverVersion.legacy + && Objects.equals(updateTime, serverVersion.updateTime) && Objects.equals(versionNumber, serverVersion.versionNumber) && Objects.equals(updateOrigin, serverVersion.updateOrigin) && Objects.equals(updateType, serverVersion.updateType) From 38f658dd4fe70af12a91488e50415a397fc7fb7b Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Fri, 8 Aug 2025 18:57:51 +0530 Subject: [PATCH 07/29] fix textcase --- .../remoteconfig/ServerTemplateData.java | 18 +++++++++--------- .../FirebaseRemoteConfigClientImplTest.java | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java index 59d51b51a..4572bf21a 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java @@ -40,7 +40,7 @@ final class ServerTemplateData { private Map parameters; private List serverConditions; private Map parameterGroups; - private Version version; + private ServerVersion serverVersion; ServerTemplateData(String etag) { @@ -78,7 +78,7 @@ final class ServerTemplateData { } } if (serverTemplateResponse.getVersion() != null) { - this.version = new Version(serverTemplateResponse.getVersion()); + this.serverVersion = new ServerVersion(serverTemplateResponse.getVersion()); } this.etag = serverTemplateResponse.getEtag(); } @@ -120,8 +120,8 @@ Map getParameterGroups() { return parameterGroups; } - Version getVersion() { - return version; + ServerVersion getVersion() { + return serverVersion; } ServerTemplateData setParameters(@NonNull Map parameters) { @@ -144,8 +144,8 @@ ServerTemplateData setParameterGroups( return this; } - ServerTemplateData setVersion(Version version) { - this.version = version; + ServerTemplateData setVersion(ServerVersion serverVersion) { + this.serverVersion = serverVersion; return this; } @@ -178,7 +178,7 @@ ServerTemplateResponse toServerTemplateResponse(boolean includeAll) { parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse()); } TemplateResponse.VersionResponse versionResponse = - (this.version == null) ? null : this.version.toVersionResponse(includeAll); + (this.serverVersion == null) ? null : this.serverVersion.toVersionResponse(includeAll); ServerTemplateResponse serverTemplateResponse = new ServerTemplateResponse() .setParameters(parameterResponses) @@ -204,12 +204,12 @@ public boolean equals(Object o) { && Objects.equals(parameters, template.parameters) && Objects.equals(serverConditions, template.serverConditions) && Objects.equals(parameterGroups, template.parameterGroups) - && Objects.equals(version, template.version); + && Objects.equals(serverVersion, template.serverVersion); } @Override public int hashCode() { - return Objects.hash(etag, parameters, serverConditions, parameterGroups, version); + return Objects.hash(etag, parameters, serverConditions, parameterGroups, serverVersion); } } diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index d820dda76..c0d2118f8 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -1446,7 +1446,7 @@ public void testGetServerTemplate() throws Exception { assertEquals( convertObjectToString(EXPECTED_SERVER_CONDITIONS), convertObjectToString(serverTemplateData.getServerConditions())); - assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); + assertEquals("2020-11-15T06:57:26.342763941Z", serverTemplateData.getVersion().getUpdateTime()); checkGetRequestHeaderForServer(interceptor.getLastRequest()); } @@ -1473,7 +1473,7 @@ public void testGetServerTemplateWithTimestampUpToNanosecondPrecision() throws E assertEquals(TEST_ETAG, serverTemplateData.getETag()); assertEquals("17", serverTemplateData.getVersion().getVersionNumber()); - assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); + assertEquals(timestamp, serverTemplateData.getVersion().getUpdateTime()); checkGetRequestHeaderForServer(interceptor.getLastRequest()); } } From ee44298d467ae8736ee5a7b87e29aebb3b030c1c Mon Sep 17 00:00:00 2001 From: Athira M Date: Mon, 11 Aug 2025 22:39:25 +0530 Subject: [PATCH 08/29] Fix lint errors --- .../java/com/google/firebase/remoteconfig/ServerVersion.java | 5 +++-- .../com/google/firebase/remoteconfig/ServerVersionTest.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java b/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java index 5475c2f98..2685fbfe8 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java @@ -138,8 +138,9 @@ public String getDescription() { /** * Gets the rollback source of the template. * - *

The serverVersion number of the Remote Config template that has become the current serverVersion - * due to a rollback. Only present if this serverVersion is the result of a rollback. + *

The serverVersion number of the Remote Config template that has become the current + * serverVersion due to a rollback. Only present if this serverVersion is the result of a + * rollback. * * @return The rollback source of the template or null. */ diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java index a1ddd0a73..0e9c3c5d1 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java @@ -69,7 +69,8 @@ public void testEquality() { final ServerVersion versionThree = ServerVersion.withDescription("abcd"); final ServerVersion versionFour = ServerVersion.withDescription("abcd"); - final ServerVersion versionFive = new ServerVersion(new VersionResponse()).setDescription("abcd"); + final ServerVersion versionFive = new ServerVersion(new VersionResponse()) + .setDescription("abcd"); assertEquals(versionThree, versionFour); assertEquals(versionThree, versionFive); From 23b1b959d31f17e80a7d0cf12b715907e072c0f5 Mon Sep 17 00:00:00 2001 From: Athira M Date: Tue, 12 Aug 2025 12:35:11 +0530 Subject: [PATCH 09/29] Add unit tests --- .../remoteconfig/ServerTemplateImplTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java index ae70d2366..54403e39b 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -63,6 +63,21 @@ public void testServerTemplateWithoutCacheValueThrowsException() assertEquals("JSON String must not be null or empty.", error.getMessage()); } + @Test + public void testEvaluateWithoutContextReturnsDefaultValue() + throws FirebaseRemoteConfigException { + KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); + + ServerConfig evaluatedConfig = template.evaluate(); + + assertEquals("Default value", evaluatedConfig.getString("Custom")); + } + @Test public void testEvaluateCustomSignalReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); From 7a28b893da1dc3e5d9d0a88a2f639cf8e76a6e90 Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:42:05 +0530 Subject: [PATCH 10/29] fix for [438426692](getDouble() logs a malformed warning on type conversion failure) Using getDouble on a string parameter value, returns the appropriate default static value but logs a warning which looks incorrect ("%s" in the warning message?). --- src/main/java/com/google/firebase/remoteconfig/Value.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/Value.java b/src/main/java/com/google/firebase/remoteconfig/Value.java index 7f04aefeb..486e6556a 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Value.java +++ b/src/main/java/com/google/firebase/remoteconfig/Value.java @@ -100,7 +100,7 @@ long asLong() { try { return Long.parseLong(value); } catch (NumberFormatException e) { - logger.warn("Unable to convert %s to long type.", value); + logger.warn("Unable to convert {} to long type.", value); return DEFAULT_VALUE_FOR_LONG; } } @@ -118,7 +118,7 @@ long asLong() { try { return Double.parseDouble(this.value); } catch (NumberFormatException e) { - logger.warn("Unable to convert %s to double type.", value); + logger.warn("Unable to convert {} to double type.", value); return DEFAULT_VALUE_FOR_DOUBLE; } } From ce4caeaf23fe1ca886dd5df56f7fa0513077913a Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:36:00 +0530 Subject: [PATCH 11/29] Update ServerTemplateResponse.java to fix b/438607881 In the server template builder flow using cached template, evaluation using custom signals is not working as intended. --- .../remoteconfig/internal/ServerTemplateResponse.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java index 87f624a78..b8ea92745 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java @@ -32,7 +32,7 @@ public final class ServerTemplateResponse { @Key("parameters") private Map parameters; - @Key("conditions") + @Key("serverConditions") private List serverConditions; @Key("parameterGroups") @@ -102,7 +102,7 @@ public static final class ServerConditionResponse { @Key("name") private String name; - @Key("condition") + @Key("serverCondition") private OneOfConditionResponse condition; public String getName() { @@ -248,7 +248,7 @@ public static final class PercentConditionResponse { @Key("microPercentRange") private MicroPercentRangeResponse microPercentRange; - @Key("percentOperator") + @Key("percentConditionOperator") private String percentOperator; @Key("seed") From 61356805eef7b6a0002c2a615d124b231af8d994 Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:37:39 +0530 Subject: [PATCH 12/29] Update getServerRemoteConfig.json to fix b/438607881 --- src/test/resources/getServerRemoteConfig.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json index 2e3c35566..565a3b825 100644 --- a/src/test/resources/getServerRemoteConfig.json +++ b/src/test/resources/getServerRemoteConfig.json @@ -1,8 +1,8 @@ { - "conditions": [ + "serverConditions": [ { "name": "custom_signal", - "condition": { + "serverCondition": { "orCondition": { "conditions": [ { @@ -26,7 +26,7 @@ }, { "name": "percent", - "condition": { + "serverCondition": { "orCondition": { "conditions": [ { @@ -51,7 +51,7 @@ }, { "name": "chained_conditions", - "condition": { + "serverCondition": { "orCondition": { "conditions": [ { From 28166e012669ef30c3a5175685033f7e064a1cee Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:38:18 +0530 Subject: [PATCH 13/29] Update getServerTemplateData.json to fix b/438607881 --- src/test/resources/getServerTemplateData.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json index 27e3507d6..2359718f6 100644 --- a/src/test/resources/getServerTemplateData.json +++ b/src/test/resources/getServerTemplateData.json @@ -1,8 +1,8 @@ { - "conditions": [ + "serverConditions": [ { "name": "custom_signal", - "condition": { + "serverCondition": { "orCondition": { "conditions": [ { @@ -26,7 +26,7 @@ }, { "name": "percent", - "condition": { + "serverCondition": { "orCondition": { "conditions": [ { @@ -51,7 +51,7 @@ }, { "name": "chained_conditions", - "condition": { + "serverCondition": { "orCondition": { "conditions": [ { From 8932e7ee824bd9be437dadd181d8b25a94902288 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Thu, 14 Aug 2025 09:48:30 +0530 Subject: [PATCH 14/29] fix for bugs --- .../remoteconfig/FirebaseRemoteConfigClientImplTest.java | 2 +- src/test/resources/getServerRemoteConfig.json | 4 ++-- src/test/resources/getServerTemplateData.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index c0d2118f8..00b122792 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -1432,7 +1432,7 @@ private void checkExceptionFromHttpResponse( } // Get server template tests - + @Test public void testGetServerTemplate() throws Exception { response.addHeader("etag", TEST_ETAG); diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json index 565a3b825..8a73ae8ff 100644 --- a/src/test/resources/getServerRemoteConfig.json +++ b/src/test/resources/getServerRemoteConfig.json @@ -34,7 +34,7 @@ "conditions": [ { "percent": { - "percentOperator": "BETWEEN", + "percentConditionOperator": "BETWEEN", "seed": "3maarirs9xzs", "microPercentRange": { "microPercentLowerBound": 12000000, @@ -77,7 +77,7 @@ }, { "percent": { - "percentOperator": "BETWEEN", + "percentConditionOperator": "BETWEEN", "seed": "cla24qoibb61", "microPercentRange": { "microPercentLowerBound": 25000000, diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json index 2359718f6..ddd662419 100644 --- a/src/test/resources/getServerTemplateData.json +++ b/src/test/resources/getServerTemplateData.json @@ -34,7 +34,7 @@ "conditions": [ { "percent": { - "percentOperator": "BETWEEN", + "percentConditionOperator": "BETWEEN", "seed": "3maarirs9xzs", "microPercentRange": { "microPercentLowerBound": 12000000, @@ -75,7 +75,7 @@ ] }, "percent": { - "percentOperator": "BETWEEN", + "percentConditionOperator": "BETWEEN", "seed": "cla24qoibb61", "microPercentRange": { "microPercentLowerBound": 25000000, From dfecfc68f3813ea12cf29541435efff66b5368cb Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 21:02:32 +0530 Subject: [PATCH 15/29] Resolve comment related to revert of ServerVersion Class --- .../remoteconfig/ServerTemplateData.java | 18 ++--- .../remoteconfig/ServerTemplateImpl.java | 5 +- .../FirebaseRemoteConfigClientImplTest.java | 5 +- .../FirebaseRemoteConfigTest.java | 29 ++++--- .../remoteconfig/ServerTemplateImplTest.java | 76 ++++++++++++++----- src/test/resources/getServerTemplateData.json | 12 ++- 6 files changed, 91 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java index 4572bf21a..17b268515 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java @@ -40,7 +40,7 @@ final class ServerTemplateData { private Map parameters; private List serverConditions; private Map parameterGroups; - private ServerVersion serverVersion; + private Version version; ServerTemplateData(String etag) { @@ -78,7 +78,7 @@ final class ServerTemplateData { } } if (serverTemplateResponse.getVersion() != null) { - this.serverVersion = new ServerVersion(serverTemplateResponse.getVersion()); + this.version = new Version(serverTemplateResponse.getVersion()); } this.etag = serverTemplateResponse.getEtag(); } @@ -120,8 +120,8 @@ Map getParameterGroups() { return parameterGroups; } - ServerVersion getVersion() { - return serverVersion; + Version getVersion() { + return version; } ServerTemplateData setParameters(@NonNull Map parameters) { @@ -144,8 +144,8 @@ ServerTemplateData setParameterGroups( return this; } - ServerTemplateData setVersion(ServerVersion serverVersion) { - this.serverVersion = serverVersion; + ServerTemplateData setVersion(Version serverVersion) { + this.version = serverVersion; return this; } @@ -178,7 +178,7 @@ ServerTemplateResponse toServerTemplateResponse(boolean includeAll) { parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse()); } TemplateResponse.VersionResponse versionResponse = - (this.serverVersion == null) ? null : this.serverVersion.toVersionResponse(includeAll); + (this.version == null) ? null : this.version.toVersionResponse(includeAll); ServerTemplateResponse serverTemplateResponse = new ServerTemplateResponse() .setParameters(parameterResponses) @@ -204,12 +204,12 @@ public boolean equals(Object o) { && Objects.equals(parameters, template.parameters) && Objects.equals(serverConditions, template.serverConditions) && Objects.equals(parameterGroups, template.parameterGroups) - && Objects.equals(serverVersion, template.serverVersion); + && Objects.equals(version, template.version); } @Override public int hashCode() { - return Objects.hash(etag, parameters, serverConditions, parameterGroups, serverVersion); + return Objects.hash(etag, parameters, serverConditions, parameterGroups, version); } } diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java index 6d3d675d7..27a5ae221 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -22,8 +22,6 @@ import com.google.firebase.ErrorCode; import com.google.firebase.internal.Nullable; import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import java.util.HashMap; import java.util.Map; @@ -130,8 +128,7 @@ public String getCachedTemplate() { @Override public String toJson() { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - return gson.toJson(this.cache); + return this.cache.toJSON(); } private void mergeDerivedConfigValues(ImmutableMap evaluatedCondition, diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index 00b122792..9969ac406 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -1446,7 +1446,7 @@ public void testGetServerTemplate() throws Exception { assertEquals( convertObjectToString(EXPECTED_SERVER_CONDITIONS), convertObjectToString(serverTemplateData.getServerConditions())); - assertEquals("2020-11-15T06:57:26.342763941Z", serverTemplateData.getVersion().getUpdateTime()); + assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); checkGetRequestHeaderForServer(interceptor.getLastRequest()); } @@ -1470,10 +1470,9 @@ public void testGetServerTemplateWithTimestampUpToNanosecondPrecision() throws E String receivedTemplate = client.getServerTemplate(); ServerTemplateData serverTemplateData = ServerTemplateData.fromJSON(receivedTemplate); - assertEquals(TEST_ETAG, serverTemplateData.getETag()); assertEquals("17", serverTemplateData.getVersion().getVersionNumber()); - assertEquals(timestamp, serverTemplateData.getVersion().getUpdateTime()); + assertEquals(1605423446000L, serverTemplateData.getVersion().getUpdateTime()); checkGetRequestHeaderForServer(interceptor.getLastRequest()); } } diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index f34035f55..ab71e5e59 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 com.google.gson.JsonElement; +import com.google.gson.JsonParser; import java.util.concurrent.ExecutionException; import org.junit.After; import org.junit.Test; @@ -42,13 +44,6 @@ public class FirebaseRemoteConfigTest { .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); - private static final String TEST_SERVER_TEMPLATE = - "{\n" - + " \"etag\": \"etag-123456789012-1\",\n" - + " \"parameters\": {},\n" - + " \"serverConditions\": [],\n" - + " \"parameterGroups\": {}\n" - + "}"; private static final FirebaseRemoteConfigException TEST_EXCEPTION = new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); @@ -629,14 +624,16 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) @Test public void testGetServerTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplate(); String templateData = template.toJson(); - assertEquals(TEST_SERVER_TEMPLATE, templateData); + JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement actualJson = JsonParser.parseString(templateData); + + assertEquals(expectedJson, actualJson); } @Test @@ -653,14 +650,16 @@ public void testGetServerTemplateFailure() { @Test public void testGetServerTemplateAsync() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplateAsync().get(); String templateData = template.toJson(); - assertEquals(TEST_SERVER_TEMPLATE, templateData); + JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement actualJson = JsonParser.parseString(templateData); + + assertEquals(expectedJson, actualJson); } @Test diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java index 54403e39b..54f021a21 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -26,6 +26,8 @@ import com.google.firebase.testing.TestUtils; import org.junit.BeforeClass; import org.junit.Test; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; /** * Tests for {@link ServerTemplateImpl}. @@ -37,13 +39,6 @@ public class ServerTemplateImplTest { .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); - private static final String TEST_SERVER_TEMPLATE = - "{\n" - + " \"etag\": \"etag-123456789012-1\",\n" - + " \"parameters\": {},\n" - + " \"serverConditions\": [],\n" - + " \"parameterGroups\": {}\n" - + "}"; private static String cacheTemplate; @@ -354,27 +349,66 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) return new FirebaseRemoteConfig(app, client); } - @Test - public void testLoad() throws Exception { - KeysAndValues defaultConfig = - new KeysAndValues.Builder().put("Unset default config", "abc").build(); +@Test +public void testLoad() throws Exception { + // 1. Define the template data that the mock client will return. + // This is the EXPECTED state after `load()` is called. + final String expectedTemplateJsonAfterLoad = new ServerTemplateData().setETag(TEST_ETAG).toJSON(); - // Mock the HTTP client to return a predefined response + // 2. Mock the HTTP client to return the predefined response. MockRemoteConfigClient client = - MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient.fromServerTemplate(expectedTemplateJsonAfterLoad); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ServerTemplate template1 = + + // 3. Build the template instance. + // It's initialized with a complex `cacheTemplate` to ensure `load()` properly overwrites it. + KeysAndValues defaultConfig = + new KeysAndValues.Builder().put("Unset default config", "abc").build(); + ServerTemplate template = remoteConfig .serverTemplateBuilder() .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) // This is the initial state before `load()` + .build(); + + // 4. Call the load method, which fetches the new template from the mock client. + ApiFuture loadFuture = template.load(); + loadFuture.get(); // Wait for the async operation to complete. + + // 5. Get the ACTUAL state of the template after `load()` has executed. + String actualJsonAfterLoad = template.toJson(); + + // 6. Assert that the template's state has been updated to match what the mock client returned. + // Parsing to JsonElement performs a deep, order-insensitive comparison. + JsonElement expectedJson = JsonParser.parseString(expectedTemplateJsonAfterLoad); + JsonElement actualJson = JsonParser.parseString(actualJsonAfterLoad); + + assertEquals(expectedJson, actualJson); +} + +@Test +public void testBuilderParsesCachedTemplateCorrectly() throws FirebaseRemoteConfigException { + // Arrange: + // 1. Create a canonical JSON string by parsing the input file and then + // re-serializing it. This gives us the precise expected output format, + // accounting for any formatting or default value differences. + ServerTemplateData canonicalData = ServerTemplateData.fromJSON(cacheTemplate); + String expectedJsonString = canonicalData.toJSON(); + + // Act: + // 2. Build a ServerTemplate instance from the original cached JSON string, + // which triggers the parsing logic we want to test. + ServerTemplate template = new ServerTemplateImpl.Builder(null) .cachedTemplate(cacheTemplate) .build(); - // Call the load method - ApiFuture loadFuture = template1.load(); - loadFuture.get(); - String cachedTemplate = template1.toJson(); - assertEquals(TEST_SERVER_TEMPLATE, cachedTemplate); - } + // Assert: + // 3. Compare the JSON from the newly built template against the canonical version. + // This verifies that the internal state was parsed and stored correctly. + // Using JsonElement ensures the comparison is not affected by key order. + JsonElement expectedJsonTree = JsonParser.parseString(expectedJsonString); + JsonElement actualJsonTree = JsonParser.parseString(template.toJson()); + + assertEquals(expectedJsonTree, actualJsonTree); +} } diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json index ddd662419..81da133f9 100644 --- a/src/test/resources/getServerTemplateData.json +++ b/src/test/resources/getServerTemplateData.json @@ -167,7 +167,15 @@ } }, "version": { - "versionNumber": "27", - "isLegacy": true + "versionNumber": "17", + "updateOrigin": "ADMIN_SDK_NODE", + "updateType": "INCREMENTAL_UPDATE", + "updateUser": { + "email": "firebase-user@account.com", + "name": "dev-admin", + "imageUrl": "http://image.jpg" + }, + "updateTime": "2020-11-15T06:57:26.342763941Z", + "description": "promo config" } } From a0b71bd2c72a48f92eca7b4a486cef8fef04adc4 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 21:05:02 +0530 Subject: [PATCH 16/29] remove serverVersion --- .../remoteconfig/ServerTemplateData.java | 4 +- .../firebase/remoteconfig/ServerVersion.java | 213 ------------------ .../remoteconfig/ServerVersionTest.java | 100 -------- 3 files changed, 2 insertions(+), 315 deletions(-) delete mode 100644 src/main/java/com/google/firebase/remoteconfig/ServerVersion.java delete mode 100644 src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java index 17b268515..59d51b51a 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java @@ -144,8 +144,8 @@ ServerTemplateData setParameterGroups( return this; } - ServerTemplateData setVersion(Version serverVersion) { - this.version = serverVersion; + ServerTemplateData setVersion(Version version) { + this.version = version; return this; } diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java b/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java deleted file mode 100644 index 2685fbfe8..000000000 --- a/src/main/java/com/google/firebase/remoteconfig/ServerVersion.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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.firebase.internal.NonNull; -import com.google.firebase.internal.Nullable; -import com.google.firebase.remoteconfig.internal.TemplateResponse; -import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; - -import java.util.Objects; - -/** - * Represents a Server Remote Config template version. - * Output only, except for the version description. Contains metadata about a particular - * version of the Remote Config template. All fields are set at the time the specified Remote - * Config template is published. A version's description field may be specified when - * publishing a template. - */ -public final class ServerVersion { - - private final String versionNumber; - private final String updateTime; - private final String updateOrigin; - private final String updateType; - private final User updateUser; - private final String rollbackSource; - private final boolean legacy; - private String description; - - private ServerVersion() { - this.versionNumber = null; - this.updateTime = null; - this.updateOrigin = null; - this.updateType = null; - this.updateUser = null; - this.rollbackSource = null; - this.legacy = false; - } - - ServerVersion(@NonNull VersionResponse versionResponse) { - checkNotNull(versionResponse); - this.versionNumber = versionResponse.getVersionNumber(); - this.updateTime = versionResponse.getUpdateTime(); - this.updateOrigin = versionResponse.getUpdateOrigin(); - this.updateType = versionResponse.getUpdateType(); - TemplateResponse.UserResponse userResponse = versionResponse.getUpdateUser(); - this.updateUser = (userResponse != null) ? new User(userResponse) : null; - this.description = versionResponse.getDescription(); - this.rollbackSource = versionResponse.getRollbackSource(); - this.legacy = versionResponse.isLegacy(); - } - - /** - * Creates a new {@link Version} with a description. - */ - public static ServerVersion withDescription(String description) { - return new ServerVersion().setDescription(description); - } - - /** - * Gets the version number of the template. - * - * @return The version number or null. - */ - @Nullable - public String getVersionNumber() { - return versionNumber; - } - - /** - * Gets the update time of the version. The timestamp of when this version of the Remote Config - * template was written to the Remote Config backend. - * - * @return The update time of the version or null. - */ - @Nullable - public String getUpdateTime() { - return updateTime; - } - - /** - * Gets the origin of the template update action. - * - * @return The origin of the template update action or null. - */ - @Nullable - public String getUpdateOrigin() { - return updateOrigin; - } - - /** - * Gets the type of the template update action. - * - * @return The type of the template update action or null. - */ - @Nullable - public String getUpdateType() { - return updateType; - } - - /** - * Gets the update user of the template. - * An aggregation of all metadata fields about the account that performed the update. - * - * @return The update user of the template or null. - */ - @Nullable - public User getUpdateUser() { - return updateUser; - } - - /** - * Gets the user-provided description of the corresponding Remote Config template. - * - * @return The description of the template or null. - */ - @Nullable - public String getDescription() { - return description; - } - - /** - * Gets the rollback source of the template. - * - *

The serverVersion number of the Remote Config template that has become the current - * serverVersion due to a rollback. Only present if this serverVersion is the result of a - * rollback. - * - * @return The rollback source of the template or null. - */ - @Nullable - public String getRollbackSource() { - return rollbackSource; - } - - /** - * Indicates whether this Remote Config template was published before serverVersion history was - * supported. - * - * @return true if the template was published before serverVersion history was supported, - * and false otherwise. - */ - public boolean isLegacy() { - return legacy; - } - - /** - * Sets the user-provided description of the template. - * - * @param description The description of the template. - * @return This {@link ServerVersion}. - */ - public ServerVersion setDescription(String description) { - this.description = description; - return this; - } - - VersionResponse toVersionResponse(boolean includeAll) { - VersionResponse versionResponse = new VersionResponse().setDescription(this.description); - if (includeAll) { - versionResponse.setUpdateTime(this.updateTime) - .setLegacy(this.legacy) - .setRollbackSource(this.rollbackSource) - .setUpdateOrigin(this.updateOrigin) - .setUpdateType(this.updateType) - .setUpdateUser((this.updateUser == null) ? null : this.updateUser.toUserResponse()) - .setVersionNumber(this.versionNumber); - } - return versionResponse; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ServerVersion serverVersion = (ServerVersion) o; - return legacy == serverVersion.legacy - && Objects.equals(updateTime, serverVersion.updateTime) - && Objects.equals(versionNumber, serverVersion.versionNumber) - && Objects.equals(updateOrigin, serverVersion.updateOrigin) - && Objects.equals(updateType, serverVersion.updateType) - && Objects.equals(updateUser, serverVersion.updateUser) - && Objects.equals(description, serverVersion.description) - && Objects.equals(rollbackSource, serverVersion.rollbackSource); - } - - @Override - public int hashCode() { - return Objects - .hash(versionNumber, updateTime, updateOrigin, updateType, updateUser, description, - rollbackSource, legacy); - } -} diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java deleted file mode 100644 index 0e9c3c5d1..000000000 --- a/src/test/java/com/google/firebase/remoteconfig/ServerVersionTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.assertNotEquals; -import static org.junit.Assert.assertNull; - -import com.google.firebase.remoteconfig.internal.TemplateResponse; -import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; -import org.junit.Test; - -public class ServerVersionTest { - - @Test(expected = NullPointerException.class) - public void testConstructorWithNullServerVersionResponse() { - new ServerVersion(null); - } - - @Test - public void testConstructorWithValidZuluUpdateTime() { - ServerVersion serverVersion = new ServerVersion(new VersionResponse() - .setUpdateTime("2020-12-08T15:49:51.887878Z")); - assertEquals("2020-12-08T15:49:51.887878Z", serverVersion.getUpdateTime()); - } - - @Test - public void testConstructorWithValidUTCUpdateTime() { - ServerVersion serverVersion = new ServerVersion(new VersionResponse() - .setUpdateTime("Tue, 08 Dec 2020 15:49:51 GMT")); - assertEquals("Tue, 08 Dec 2020 15:49:51 GMT", serverVersion.getUpdateTime()); - } - - @Test - public void testWithDescription() { - final ServerVersion serverVersion = ServerVersion.withDescription("version description text"); - - assertEquals("version description text", serverVersion.getDescription()); - assertNull(serverVersion.getVersionNumber()); - assertEquals(null, serverVersion.getUpdateTime()); - assertNull(serverVersion.getUpdateOrigin()); - assertNull(serverVersion.getUpdateType()); - assertNull(serverVersion.getUpdateUser()); - assertNull(serverVersion.getRollbackSource()); - assertFalse(serverVersion.isLegacy()); - } - - @Test - public void testEquality() { - final ServerVersion versionOne = new ServerVersion(new VersionResponse()); - final ServerVersion versionTwo = new ServerVersion(new VersionResponse()); - - assertEquals(versionOne, versionTwo); - - final ServerVersion versionThree = ServerVersion.withDescription("abcd"); - final ServerVersion versionFour = ServerVersion.withDescription("abcd"); - final ServerVersion versionFive = new ServerVersion(new VersionResponse()) - .setDescription("abcd"); - - assertEquals(versionThree, versionFour); - assertEquals(versionThree, versionFive); - - final ServerVersion versionSix = ServerVersion.withDescription("efgh"); - - assertNotEquals(versionThree, versionSix); - assertNotEquals(versionOne, versionSix); - - final VersionResponse versionResponse = new VersionResponse() - .setVersionNumber("23") - .setUpdateTime("2014-10-02T15:01:23.045123456Z") - .setUpdateOrigin("ADMIN_SDK") - .setUpdateUser(new TemplateResponse.UserResponse() - .setEmail("user@email.com") - .setName("user-1234") - .setImageUrl("http://user.jpg")) - .setUpdateType("INCREMENTAL_UPDATE"); - final ServerVersion versionSeven = new ServerVersion(versionResponse); - final ServerVersion versionEight = new ServerVersion(versionResponse); - - assertEquals(versionSeven, versionEight); - assertNotEquals(versionOne, versionSeven); - assertNotEquals(versionThree, versionSeven); - assertNotEquals(versionSix, versionSeven); - } -} From 8fc3cb7baf15152abe656f8e057d2a3373369271 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 21:20:21 +0530 Subject: [PATCH 17/29] Resolve comments related to Evaluator --- .../google/firebase/remoteconfig/ConditionEvaluator.java | 6 +++++- .../google/firebase/remoteconfig/ServerTemplateImpl.java | 7 ++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java index 2bd828e2c..b18ca0322 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; @@ -57,12 +58,15 @@ Map evaluateConditions( @Nullable KeysAndValues context) { checkNotNull(conditions, "List of conditions must not be null."); checkArgument(!conditions.isEmpty(), "List of conditions must not be empty."); + if(context == null || conditions.isEmpty()){ + return ImmutableMap.of(); + } KeysAndValues evaluationContext = context != null ? context : new KeysAndValues.Builder().build(); Map evaluatedConditions = conditions.stream() - .collect(Collectors.toMap( + .collect(ImmutableMap.toImmutableMap( ServerCondition::getName, condition -> evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java index 27a5ae221..d9a62dbd2 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -93,11 +93,8 @@ public ServerConfig evaluate(@Nullable KeysAndValues context) } ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); - ImmutableMap evaluatedCondition = - context != null && !cache.getServerConditions().isEmpty() - ? ImmutableMap.copyOf( - conditionEvaluator.evaluateConditions(cache.getServerConditions(), context)) - : ImmutableMap.of(); + ImmutableMap evaluatedCondition = ImmutableMap.copyOf( + conditionEvaluator.evaluateConditions(cache.getServerConditions(), context)); ImmutableMap parameters = ImmutableMap.copyOf(cache.getParameters()); mergeDerivedConfigValues(evaluatedCondition, parameters, configValues); From 95910ad5ad4e4999ee08b7613c0d12fc523546fe Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 22:40:04 +0530 Subject: [PATCH 18/29] fix indentation --- .../google/firebase/remoteconfig/ServerTemplateImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java index d9a62dbd2..d38ac51c4 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -42,7 +42,7 @@ public static class Builder implements ServerTemplate.Builder { private KeysAndValues defaultConfig; private String cachedTemplate; private FirebaseRemoteConfigClient client; - + Builder(FirebaseRemoteConfigClient remoteConfigClient) { this.client = remoteConfigClient; } @@ -77,11 +77,11 @@ private ServerTemplateImpl(Builder builder) { } @Override - public ServerConfig evaluate(@Nullable KeysAndValues context) + public ServerConfig evaluate(@Nullable KeysAndValues context) throws FirebaseRemoteConfigException { if (this.cache == null) { throw new FirebaseRemoteConfigException(ErrorCode.FAILED_PRECONDITION, - "No Remote Config Server template in cache. Call load() before calling evaluate()."); + "No Remote Config Server template in cache. Call load() before calling evaluate()."); } Map configValues = new HashMap<>(); @@ -94,7 +94,7 @@ public ServerConfig evaluate(@Nullable KeysAndValues context) ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); ImmutableMap evaluatedCondition = ImmutableMap.copyOf( - conditionEvaluator.evaluateConditions(cache.getServerConditions(), context)); + conditionEvaluator.evaluateConditions(cache.getServerConditions(), context)); ImmutableMap parameters = ImmutableMap.copyOf(cache.getParameters()); mergeDerivedConfigValues(evaluatedCondition, parameters, configValues); From 3257dc5f42ee66034a542dcf08cc62798cf56c51 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 22:46:56 +0530 Subject: [PATCH 19/29] fix indentation --- .../remoteconfig/ConditionEvaluator.java | 34 +- .../google/firebase/remoteconfig/Value.java | 1 - .../internal/ServerTemplateResponse.java | 11 +- .../FirebaseRemoteConfigClientImplTest.java | 381 ++++++++---------- .../FirebaseRemoteConfigTest.java | 94 ++--- .../remoteconfig/ServerTemplateImplTest.java | 239 +++++------ src/test/resources/getServerRemoteConfig.json | 2 +- src/test/resources/getServerTemplateData.json | 4 +- 8 files changed, 349 insertions(+), 417 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java index b18ca0322..436a3e371 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -39,7 +39,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - + final class ConditionEvaluator { private static final int MAX_CONDITION_RECURSION_DEPTH = 10; private static final Logger logger = LoggerFactory.getLogger(ConditionEvaluator.class); @@ -49,7 +49,7 @@ final class ConditionEvaluator { * Evaluates server conditions and assigns a boolean value to each condition. * * @param conditions List of conditions which are to be evaluated. - * @param context A map with additional metadata used during evaluation. + * @param context A map with additional metadata used during evaluation. * @return A map of condition to evaluated value. */ @NonNull @@ -58,19 +58,17 @@ Map evaluateConditions( @Nullable KeysAndValues context) { checkNotNull(conditions, "List of conditions must not be null."); checkArgument(!conditions.isEmpty(), "List of conditions must not be empty."); - if(context == null || conditions.isEmpty()){ + if (context == null || conditions.isEmpty()) { return ImmutableMap.of(); } - KeysAndValues evaluationContext = context != null - ? context + KeysAndValues evaluationContext = context != null + ? context : new KeysAndValues.Builder().build(); Map evaluatedConditions = conditions.stream() .collect(ImmutableMap.toImmutableMap( ServerCondition::getName, - condition -> - evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0) - )); + condition -> evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0))); return evaluatedConditions; } @@ -99,7 +97,6 @@ private boolean evaluateCondition(OneOfCondition condition, KeysAndValues contex return false; } - private boolean evaluateOrCondition(OrCondition condition, KeysAndValues context, int nestingLevel) { return condition.getConditions().stream() @@ -251,9 +248,8 @@ private BigInteger hashSeededRandomizationId(String seededRandomizationId) { } private boolean compareStrings(ImmutableList targetValues, String customSignal, - BiPredicate compareFunction) { - return targetValues.stream().anyMatch(targetValue -> - compareFunction.test(customSignal, targetValue)); + BiPredicate compareFunction) { + return targetValues.stream().anyMatch(targetValue -> compareFunction.test(customSignal, targetValue)); } private boolean compareStringRegex(String customSignal, String targetSignal) { @@ -265,7 +261,7 @@ private boolean compareStringRegex(String customSignal, String targetSignal) { } private boolean compareNumbers(ImmutableList targetValues, String customSignal, - IntPredicate compareFunction) { + IntPredicate compareFunction) { if (targetValues.size() != 1) { logger.warn(String.format( "Target values must contain 1 element for numeric operations. Target Value: %s", @@ -286,8 +282,8 @@ private boolean compareNumbers(ImmutableList targetValues, String custom } private boolean compareSemanticVersions(ImmutableList targetValues, - String customSignal, - IntPredicate compareFunction) { + String customSignal, + IntPredicate compareFunction) { if (targetValues.size() != 1) { logger.warn(String.format("Target values must contain 1 element for semantic operation.")); return false; @@ -320,8 +316,8 @@ private int compareSemanticVersions(List version1, List versio for (int i = 0; i < maxLength; i++) { // Default to 0 if segment is missing - int v1 = i < version1Size ? version1.get(i) : 0; - int v2 = i < version2Size ? version2.get(i) : 0; + int v1 = i < version1Size ? version1.get(i) : 0; + int v2 = i < version2Size ? version2.get(i) : 0; int comparison = Integer.compare(v1, v2); if (comparison != 0) { @@ -334,8 +330,8 @@ private int compareSemanticVersions(List version1, List versio private List parseSemanticVersion(String versionString) { return Arrays.stream(versionString.split("\\.")) - .map(Integer::parseInt) - .collect(Collectors.toList()); + .map(Integer::parseInt) + .collect(Collectors.toList()); } private boolean validateSemanticVersion(String version) { diff --git a/src/main/java/com/google/firebase/remoteconfig/Value.java b/src/main/java/com/google/firebase/remoteconfig/Value.java index 486e6556a..7935e8491 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Value.java +++ b/src/main/java/com/google/firebase/remoteconfig/Value.java @@ -133,4 +133,3 @@ ValueSource getSource() { return source; } } - diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java index b8ea92745..c58faa94b 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java @@ -25,7 +25,8 @@ import java.util.Map; /** - * The Data Transfer Object for parsing Remote Config template responses from the Remote Config + * The Data Transfer Object for parsing Remote Config template responses from + * the Remote Config * service. */ public final class ServerTemplateResponse { @@ -94,7 +95,8 @@ public ServerTemplateResponse setEtag(String etag) { } /** - * The Data Transfer Object for parsing Remote Config condition responses from the Remote Config + * The Data Transfer Object for parsing Remote Config condition responses from + * the Remote Config * service. */ public static final class ServerConditionResponse { @@ -164,7 +166,7 @@ public OneOfConditionResponse setAndCondition(AndConditionResponse andCondition) } public OneOfConditionResponse setCustomSignalCondition( - CustomSignalConditionResponse customSignalCondition) { + CustomSignalConditionResponse customSignalCondition) { this.customSignalCondition = customSignalCondition; return this; } @@ -276,7 +278,7 @@ public PercentConditionResponse setMicroPercent(int microPercent) { } public PercentConditionResponse setMicroPercentRange( - MicroPercentRangeResponse microPercentRange) { + MicroPercentRangeResponse microPercentRange) { this.microPercentRange = microPercentRange; return this; } @@ -318,4 +320,3 @@ public MicroPercentRangeResponse setMicroPercentUpperBound(int microPercentUpper } } } - diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index 9969ac406..4ccf906d8 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -61,156 +61,144 @@ public class FirebaseRemoteConfigClientImplTest { - private static final String TEST_REMOTE_CONFIG_URL = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; - private static final String TEST_SERVER_REMOTE_CONFIG_URL = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + private static final String TEST_REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + private static final String TEST_SERVER_REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; private static final List HTTP_STATUS_CODES = ImmutableList.of(401, 404, 500); private static final Map HTTP_STATUS_TO_ERROR_CODE = ImmutableMap.of( - 401, ErrorCode.UNAUTHENTICATED, - 404, ErrorCode.NOT_FOUND, - 500, ErrorCode.INTERNAL); + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); private static final String MOCK_TEMPLATE_RESPONSE = TestUtils - .loadResource("getRemoteConfig.json"); - + .loadResource("getRemoteConfig.json"); + private static final String MOCK_SERVER_TEMPLATE_RESPONSE = TestUtils - .loadResource("getServerRemoteConfig.json"); + .loadResource("getServerRemoteConfig.json"); private static final String MOCK_LIST_VERSIONS_RESPONSE = TestUtils - .loadResource("listRemoteConfigVersions.json"); + .loadResource("listRemoteConfigVersions.json"); private static final String TEST_ETAG = "etag-123456789012-1"; - private static final Map EXPECTED_PARAMETERS = - ImmutableMap.of( - "welcome_message_text", - new Parameter() - .setDefaultValue(ParameterValue.of("welcome to app")) - .setConditionalValues( - ImmutableMap.of( - "ios_en", ParameterValue.of("welcome to app en"))) - .setDescription("text for welcome message!") - .setValueType(ParameterValueType.STRING), - "header_text", - new Parameter() - .setDefaultValue(ParameterValue.inAppDefault()) - .setValueType(ParameterValueType.STRING)); - - private static final Map EXPECTED_PARAMETER_GROUPS = - ImmutableMap.of( - "new menu", - new ParameterGroup() - .setDescription("New Menu") - .setParameters( - ImmutableMap.of( - "pumpkin_spice_season", - new Parameter() - .setDefaultValue(ParameterValue.of("true")) - .setDescription("Whether it's currently pumpkin spice season.") - .setValueType(ParameterValueType.BOOLEAN)))); - - private static final List EXPECTED_CONDITIONS = - ImmutableList.of( - new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']") - .setTagColor(TagColor.INDIGO), - new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']")); - - private static final List EXPECTED_SERVER_CONDITIONS = - ImmutableList.of( - new ServerCondition("custom_signal", null) - .setServerCondition( - new OneOfCondition() - .setOrCondition( - new OrCondition( - ImmutableList.of( - new OneOfCondition() - .setAndCondition( - new AndCondition( - ImmutableList.of( - new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( - "users", - CustomSignalOperator - .NUMERIC_LESS_THAN, - new ArrayList<>( - ImmutableList.of("100"))))))))))), - new ServerCondition("percent", null) - .setServerCondition( - new OneOfCondition() - .setOrCondition( - new OrCondition( - ImmutableList.of( - new OneOfCondition() - .setAndCondition( - new AndCondition( - ImmutableList.of( - new OneOfCondition() - .setPercent( - new PercentCondition( - new MicroPercentRange( - 12000000, 100000000), - PercentConditionOperator.BETWEEN, - "3maarirs9xzs"))))))))), - new ServerCondition("chained_conditions", null) - .setServerCondition( - new OneOfCondition() - .setOrCondition( - new OrCondition( - ImmutableList.of( - new OneOfCondition() - .setAndCondition( - new AndCondition( - ImmutableList.of( - new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( - "users", - CustomSignalOperator - .NUMERIC_LESS_THAN, + private static final Map EXPECTED_PARAMETERS = ImmutableMap.of( + "welcome_message_text", + new Parameter() + .setDefaultValue(ParameterValue.of("welcome to app")) + .setConditionalValues( + ImmutableMap.of( + "ios_en", ParameterValue.of("welcome to app en"))) + .setDescription("text for welcome message!") + .setValueType(ParameterValueType.STRING), + "header_text", + new Parameter() + .setDefaultValue(ParameterValue.inAppDefault()) + .setValueType(ParameterValueType.STRING)); + + private static final Map EXPECTED_PARAMETER_GROUPS = ImmutableMap.of( + "new menu", + new ParameterGroup() + .setDescription("New Menu") + .setParameters( + ImmutableMap.of( + "pumpkin_spice_season", + new Parameter() + .setDefaultValue(ParameterValue.of("true")) + .setDescription("Whether it's currently pumpkin spice season.") + .setValueType(ParameterValueType.BOOLEAN)))); + + private static final List EXPECTED_CONDITIONS = ImmutableList.of( + new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']") + .setTagColor(TagColor.INDIGO), + new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']")); + + private static final List EXPECTED_SERVER_CONDITIONS = ImmutableList.of( + new ServerCondition("custom_signal", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100"))))))))))), + new ServerCondition("percent", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange( + 12000000, 100000000), + PercentConditionOperator.BETWEEN, + "3maarirs9xzs"))))))))), + new ServerCondition("chained_conditions", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator.NUMERIC_LESS_THAN, new ArrayList<>( - ImmutableList.of("100")))), - new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( + ImmutableList.of("100")))), + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( "premium users", - CustomSignalOperator - .NUMERIC_GREATER_THAN, + CustomSignalOperator.NUMERIC_GREATER_THAN, new ArrayList<>( ImmutableList.of("20")))), - new OneOfCondition() - .setPercent( + new OneOfCondition() + .setPercent( new PercentCondition( new MicroPercentRange( 25000000, 100000000), PercentConditionOperator.BETWEEN, - "cla24qoibb61")) - )))))))); - - private static final Version EXPECTED_VERSION = - new Version( - new TemplateResponse.VersionResponse() - .setVersionNumber("17") - .setUpdateOrigin("ADMIN_SDK_NODE") - .setUpdateType("INCREMENTAL_UPDATE") - .setUpdateUser( - new TemplateResponse.UserResponse() - .setEmail("firebase-user@account.com") - .setName("dev-admin") - .setImageUrl("http://image.jpg")) - .setUpdateTime("2020-11-15T06:57:26.342763941Z") - .setDescription("promo config")); - - private static final Template EXPECTED_TEMPLATE = - new Template() - .setETag(TEST_ETAG) - .setParameters(EXPECTED_PARAMETERS) - .setConditions(EXPECTED_CONDITIONS) - .setParameterGroups(EXPECTED_PARAMETER_GROUPS) - .setVersion(EXPECTED_VERSION); + "cla24qoibb61")))))))))); + + private static final Version EXPECTED_VERSION = new Version( + new TemplateResponse.VersionResponse() + .setVersionNumber("17") + .setUpdateOrigin("ADMIN_SDK_NODE") + .setUpdateType("INCREMENTAL_UPDATE") + .setUpdateUser( + new TemplateResponse.UserResponse() + .setEmail("firebase-user@account.com") + .setName("dev-admin") + .setImageUrl("http://image.jpg")) + .setUpdateTime("2020-11-15T06:57:26.342763941Z") + .setDescription("promo config")); + + private static final Template EXPECTED_TEMPLATE = new Template() + .setETag(TEST_ETAG) + .setParameters(EXPECTED_PARAMETERS) + .setConditions(EXPECTED_CONDITIONS) + .setParameterGroups(EXPECTED_PARAMETER_GROUPS) + .setVersion(EXPECTED_VERSION); private MockLowLevelHttpResponse response; private TestResponseInterceptor interceptor; @@ -240,20 +228,18 @@ public void testGetTemplate() throws Exception { @Test public void testGetTemplateWithTimestampUpToNanosecondPrecision() throws Exception { - List timestamps = - ImmutableList.of( - "2020-11-15T06:57:26.342Z", - "2020-11-15T06:57:26.342763Z", - "2020-11-15T06:57:26.342763941Z"); + List timestamps = ImmutableList.of( + "2020-11-15T06:57:26.342Z", + "2020-11-15T06:57:26.342763Z", + "2020-11-15T06:57:26.342763941Z"); for (String timestamp : timestamps) { response.addHeader("etag", TEST_ETAG); - String templateResponse = - "{\"version\": {" - + " \"versionNumber\": \"17\"," - + " \"updateTime\": \"" - + timestamp - + "\"" - + " }}"; + String templateResponse = "{\"version\": {" + + " \"versionNumber\": \"17\"," + + " \"updateTime\": \"" + + timestamp + + "\"" + + " }}"; response.setContent(templateResponse); Template receivedTemplate = client.getTemplate(); @@ -395,14 +381,14 @@ public void testGetTemplateErrorWithMalformedResponse() { public void testGetTemplateErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.GET); + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -412,16 +398,16 @@ public void testGetTemplateErrorWithDetails() { public void testGetTemplateErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", - HttpMethods.GET); + RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", + HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -436,9 +422,8 @@ public void testGetTemplateAtVersionWithNullString() throws Exception { @Test public void testGetTemplateAtVersionWithInvalidString() throws Exception { - List invalidVersionStrings = - ImmutableList.of( - "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -670,17 +655,17 @@ public void testPublishTemplateWithValidTemplateAndForceTrue() throws Exception public void testPublishTemplateWithValidTemplateAndValidateOnlyTrue() throws Exception { response.addHeader("etag", TEST_ETAG); response.setContent(MOCK_TEMPLATE_RESPONSE); - Template expectedTemplate = - new Template() - .setETag("etag-123456789012-45") - .setParameters(EXPECTED_PARAMETERS) - .setConditions(EXPECTED_CONDITIONS) - .setParameterGroups(EXPECTED_PARAMETER_GROUPS) - .setVersion(EXPECTED_VERSION); + Template expectedTemplate = new Template() + .setETag("etag-123456789012-45") + .setParameters(EXPECTED_PARAMETERS) + .setConditions(EXPECTED_CONDITIONS) + .setParameterGroups(EXPECTED_PARAMETER_GROUPS) + .setVersion(EXPECTED_VERSION); Template validatedTemplate = client.publishTemplate(expectedTemplate, true, false); - // check if the etag matches the input template's etag and not the etag from the server response + // check if the etag matches the input template's etag and not the etag from the + // server response assertNotEquals(TEST_ETAG, validatedTemplate.getETag()); assertEquals("etag-123456789012-45", validatedTemplate.getETag()); assertEquals(expectedTemplate, validatedTemplate); @@ -866,9 +851,8 @@ public void testRollbackWithNullString() throws Exception { @Test public void testRollbackWithInvalidString() throws Exception { - List invalidVersionStrings = - ImmutableList.of( - "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -1097,28 +1081,26 @@ public void testListVersionsWithNullOptions() throws Exception { 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()); + 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"); + 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("&"); @@ -1305,11 +1287,10 @@ public void testBuilderNullRequestFactory() { @Test public void testFromApp() throws IOException { - FirebaseOptions options = - FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); FirebaseApp app = FirebaseApp.initializeApp(options); try { @@ -1318,8 +1299,7 @@ public void testFromApp() throws IOException { assertEquals(TEST_REMOTE_CONFIG_URL, client.getRemoteConfigUrl()); assertSame(options.getJsonFactory(), client.getJsonFactory()); - HttpRequest request = - client.getRequestFactory().buildGetRequest(new GenericUrl("https://example.com")); + HttpRequest request = client.getRequestFactory().buildGetRequest(new GenericUrl("https://example.com")); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } finally { app.delete(); @@ -1328,8 +1308,7 @@ public void testFromApp() throws IOException { private FirebaseRemoteConfigClientImpl initRemoteConfigClient( MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { - MockHttpTransport transport = - new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build(); + MockHttpTransport transport = new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build(); return FirebaseRemoteConfigClientImpl.builder() .setProjectId("test-project") @@ -1432,7 +1411,7 @@ private void checkExceptionFromHttpResponse( } // Get server template tests - + @Test public void testGetServerTemplate() throws Exception { response.addHeader("etag", TEST_ETAG); @@ -1452,20 +1431,18 @@ public void testGetServerTemplate() throws Exception { @Test public void testGetServerTemplateWithTimestampUpToNanosecondPrecision() throws Exception { - List timestamps = - ImmutableList.of( - "2020-11-15T06:57:26.342Z", - "2020-11-15T06:57:26.342763Z", - "2020-11-15T06:57:26.342763941Z"); + List timestamps = ImmutableList.of( + "2020-11-15T06:57:26.342Z", + "2020-11-15T06:57:26.342763Z", + "2020-11-15T06:57:26.342763941Z"); for (String timestamp : timestamps) { response.addHeader("etag", TEST_ETAG); - String templateResponse = - "{\"version\": {" - + " \"versionNumber\": \"17\"," - + " \"updateTime\": \"" - + timestamp - + "\"" - + " }}"; + String templateResponse = "{\"version\": {" + + " \"versionNumber\": \"17\"," + + " \"updateTime\": \"" + + timestamp + + "\"" + + " }}"; response.setContent(templateResponse); String receivedTemplate = client.getServerTemplate(); diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index ab71e5e59..e10ffe2a6 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -34,18 +34,18 @@ import org.junit.After; import org.junit.Test; -/** Unit tests -* for {@link FirebaseRemoteConfig}. -* */ +/** + * Unit tests + * for {@link FirebaseRemoteConfig}. + */ public class FirebaseRemoteConfigTest { - private static final FirebaseOptions TEST_OPTIONS = - FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - private static final FirebaseRemoteConfigException TEST_EXCEPTION = - new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final FirebaseRemoteConfigException TEST_EXCEPTION = new FirebaseRemoteConfigException( + ErrorCode.INTERNAL, "Test error message"); @After public void tearDown() { @@ -79,8 +79,7 @@ public void testDefaultRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } @@ -93,8 +92,7 @@ public void testDefaultServerRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getServerRemoteConfigUrl()); } @@ -116,19 +114,17 @@ public void testAppDelete() { @Test public void testRemoteConfigClientWithoutProjectId() { - FirebaseOptions options = - FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); + FirebaseOptions options = FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); FirebaseApp.initializeApp(options); try { FirebaseRemoteConfig.getInstance(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { - String message = - "Project ID is required to access Remote Config service. Use a service " - + "account credential or set the project ID explicitly via FirebaseOptions. " - + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " - + "environment variable."; + String message = "Project ID is required to access Remote Config service. Use a service " + + "account credential or set the project ID explicitly via FirebaseOptions. " + + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " + + "environment variable."; assertEquals(message, expected.getMessage()); } } @@ -139,8 +135,7 @@ public void testRemoteConfigClientWithoutProjectId() { @Test public void testGetTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplate(); @@ -162,8 +157,7 @@ public void testGetTemplateFailure() { @Test public void testGetTemplateAsync() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAsync().get(); @@ -187,8 +181,7 @@ public void testGetTemplateAsyncFailure() throws InterruptedException { @Test public void testGetTemplateAtVersionWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion("64"); @@ -210,8 +203,7 @@ public void testGetTemplateAtVersionWithStringValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync("55").get(); @@ -233,8 +225,7 @@ public void testGetTemplateAtVersionAsyncWithStringValueFailure() throws Interru @Test public void testGetTemplateAtVersionWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion(64L); @@ -256,8 +247,7 @@ public void testGetTemplateAtVersionWithLongValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync(55L).get(); @@ -425,8 +415,7 @@ public void testForcePublishTemplateAsyncFailure() throws InterruptedException { @Test public void testRollbackWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback("64"); @@ -448,8 +437,7 @@ public void testRollbackWithStringValueFailure() { @Test public void testRollbackAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync("55").get(); @@ -471,8 +459,7 @@ public void testRollbackAsyncWithStringValueFailure() throws InterruptedExceptio @Test public void testRollbackWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback(64L); @@ -494,8 +481,7 @@ public void testRollbackWithLongValueFailure() { @Test public void testRollbackAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync(55L).get(); @@ -519,9 +505,8 @@ public void testRollbackAsyncWithLongValueFailure() throws InterruptedException @Test public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersions(); @@ -543,9 +528,8 @@ public void testListVersionsWithNoOptionsFailure() { @Test public void testListVersionsAsyncWithNoOptions() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync().get(); @@ -567,13 +551,11 @@ public void testListVersionsAsyncWithNoOptionsFailure() throws InterruptedExcept @Test public void testListVersionsWithOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = - remoteConfig.listVersions(ListVersionsOptions.builder().build()); + ListVersionsPage listVersionsPage = remoteConfig.listVersions(ListVersionsOptions.builder().build()); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -592,13 +574,11 @@ public void testListVersionsWithOptionsFailure() { @Test public void testListVersionsAsyncWithOptions() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = - remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); + ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); assertEquals("token", listVersionsPage.getNextPageToken()); } diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java index 54f021a21..e49de56c1 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -30,15 +30,14 @@ import com.google.gson.JsonParser; /** -* Tests for {@link ServerTemplateImpl}. -*/ + * Tests for {@link ServerTemplateImpl}. + */ public class ServerTemplateImplTest { - private static final FirebaseOptions TEST_OPTIONS = - FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); private static String cacheTemplate; @@ -52,8 +51,8 @@ public void testServerTemplateWithoutCacheValueThrowsException() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - IllegalArgumentException error = assertThrows(IllegalArgumentException.class, - () -> new ServerTemplateImpl.Builder(null).defaultConfig(defaultConfig).build()); + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> new ServerTemplateImpl.Builder(null).defaultConfig(defaultConfig).build()); assertEquals("JSON String must not be null or empty.", error.getMessage()); } @@ -62,11 +61,10 @@ public void testServerTemplateWithoutCacheValueThrowsException() public void testEvaluateWithoutContextReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(); @@ -77,11 +75,10 @@ public void testEvaluateWithoutContextReturnsDefaultValue() public void testEvaluateCustomSignalReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "100").build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -93,11 +90,10 @@ public void testEvaluateCustomSignalReturnsConditionalValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -109,11 +105,10 @@ public void testEvaluateCustomSignalWithoutContextReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -125,11 +120,10 @@ public void testEvaluateCustomSignalWithInvalidContextReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "abc").build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -141,11 +135,10 @@ public void testEvaluatePercentWithoutRandomizationIdReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -156,11 +149,10 @@ public void testEvaluatePercentWithoutRandomizationIdReturnsDefaultValue() public void testEvaluatePercentReturnsConditionalValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -172,11 +164,10 @@ public void testEvaluateWithoutDefaultValueReturnsEmptyString() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -189,11 +180,10 @@ public void testEvaluateWithInvalidCacheValueThrowsException() KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); String invalidJsonString = "abc"; - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(invalidJsonString) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(invalidJsonString) + .build(); FirebaseRemoteConfigException error = assertThrows(FirebaseRemoteConfigException.class, () -> template.evaluate(context)); @@ -206,11 +196,10 @@ public void testEvaluateWithInvalidCacheValueThrowsException() public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -221,11 +210,10 @@ public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception { public void testEvaluateWithDerivedInAppDefaultReturnsDefaultValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -236,11 +224,10 @@ public void testEvaluateWithDerivedInAppDefaultReturnsDefaultValue() throws Exce public void testEvaluateWithMultipleConditionReturnsConditionalValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -251,14 +238,13 @@ public void testEvaluateWithMultipleConditionReturnsConditionalValue() throws Ex public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "100") - .put("premium users", 20) - .put("randomizationId", "user") - .build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + .put("premium users", 20) + .put("randomizationId", "user") + .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -268,13 +254,11 @@ public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exce @Test public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - KeysAndValues context = - new KeysAndValues.Builder().put("users", "99").put("premium users", "30").build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + KeysAndValues context = new KeysAndValues.Builder().put("users", "99").put("premium users", "30").build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -285,11 +269,10 @@ public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws public void testGetEvaluateConfigOnInvalidTypeReturnsDefaultValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -300,11 +283,10 @@ public void testGetEvaluateConfigOnInvalidTypeReturnsDefaultValue() throws Excep public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -315,11 +297,10 @@ public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exc public void testGetEvaluateConfigInAppDefaultConfigReturnsDefaultValueSource() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().put("In-app default", "abc").build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -328,14 +309,12 @@ public void testGetEvaluateConfigInAppDefaultConfigReturnsDefaultValueSource() t @Test public void testGetEvaluateConfigUnsetDefaultConfigReturnsDefaultValueSource() throws Exception { - KeysAndValues defaultConfig = - new KeysAndValues.Builder().put("Unset default config", "abc").build(); + KeysAndValues defaultConfig = new KeysAndValues.Builder().put("Unset default config", "abc").build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = - new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -349,27 +328,25 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) return new FirebaseRemoteConfig(app, client); } -@Test -public void testLoad() throws Exception { + @Test + public void testLoad() throws Exception { // 1. Define the template data that the mock client will return. // This is the EXPECTED state after `load()` is called. final String expectedTemplateJsonAfterLoad = new ServerTemplateData().setETag(TEST_ETAG).toJSON(); // 2. Mock the HTTP client to return the predefined response. - MockRemoteConfigClient client = - MockRemoteConfigClient.fromServerTemplate(expectedTemplateJsonAfterLoad); + MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate(expectedTemplateJsonAfterLoad); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); // 3. Build the template instance. - // It's initialized with a complex `cacheTemplate` to ensure `load()` properly overwrites it. - KeysAndValues defaultConfig = - new KeysAndValues.Builder().put("Unset default config", "abc").build(); - ServerTemplate template = - remoteConfig - .serverTemplateBuilder() - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) // This is the initial state before `load()` - .build(); + // It's initialized with a complex `cacheTemplate` to ensure `load()` properly + // overwrites it. + KeysAndValues defaultConfig = new KeysAndValues.Builder().put("Unset default config", "abc").build(); + ServerTemplate template = remoteConfig + .serverTemplateBuilder() + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) // This is the initial state before `load()` + .build(); // 4. Call the load method, which fetches the new template from the mock client. ApiFuture loadFuture = template.load(); @@ -378,37 +355,39 @@ public void testLoad() throws Exception { // 5. Get the ACTUAL state of the template after `load()` has executed. String actualJsonAfterLoad = template.toJson(); - // 6. Assert that the template's state has been updated to match what the mock client returned. + // 6. Assert that the template's state has been updated to match what the mock + // client returned. // Parsing to JsonElement performs a deep, order-insensitive comparison. JsonElement expectedJson = JsonParser.parseString(expectedTemplateJsonAfterLoad); JsonElement actualJson = JsonParser.parseString(actualJsonAfterLoad); assertEquals(expectedJson, actualJson); -} + } -@Test -public void testBuilderParsesCachedTemplateCorrectly() throws FirebaseRemoteConfigException { - // Arrange: + @Test + public void testBuilderParsesCachedTemplateCorrectly() throws FirebaseRemoteConfigException { + // Arrange: // 1. Create a canonical JSON string by parsing the input file and then // re-serializing it. This gives us the precise expected output format, // accounting for any formatting or default value differences. ServerTemplateData canonicalData = ServerTemplateData.fromJSON(cacheTemplate); String expectedJsonString = canonicalData.toJSON(); - // Act: + // Act: // 2. Build a ServerTemplate instance from the original cached JSON string, // which triggers the parsing logic we want to test. ServerTemplate template = new ServerTemplateImpl.Builder(null) - .cachedTemplate(cacheTemplate) - .build(); + .cachedTemplate(cacheTemplate) + .build(); - // Assert: - // 3. Compare the JSON from the newly built template against the canonical version. + // Assert: + // 3. Compare the JSON from the newly built template against the canonical + // version. // This verifies that the internal state was parsed and stored correctly. // Using JsonElement ensures the comparison is not affected by key order. JsonElement expectedJsonTree = JsonParser.parseString(expectedJsonString); JsonElement actualJsonTree = JsonParser.parseString(template.toJson()); assertEquals(expectedJsonTree, actualJsonTree); -} + } } diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json index 8a73ae8ff..19967e958 100644 --- a/src/test/resources/getServerRemoteConfig.json +++ b/src/test/resources/getServerRemoteConfig.json @@ -139,4 +139,4 @@ "updateTime": "2020-11-15T06:57:26.342763941Z", "description": "promo config" } -} +} \ No newline at end of file diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json index 81da133f9..91e3971e3 100644 --- a/src/test/resources/getServerTemplateData.json +++ b/src/test/resources/getServerTemplateData.json @@ -132,7 +132,7 @@ } } }, - "Derived in-app default": { + "Derived in-app default": { "defaultValue": { "value": "Default value" }, @@ -178,4 +178,4 @@ "updateTime": "2020-11-15T06:57:26.342763941Z", "description": "promo config" } -} +} \ No newline at end of file From d75cfb25b66f4c158f2a6f59db2a6e825387dd08 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 22:56:36 +0530 Subject: [PATCH 20/29] fix indentations --- .../remoteconfig/ConditionEvaluator.java | 162 +++---- .../FirebaseRemoteConfigClientImplTest.java | 411 ++++++++++-------- .../FirebaseRemoteConfigTest.java | 108 +++-- .../remoteconfig/ServerTemplateImplTest.java | 242 ++++++----- 4 files changed, 503 insertions(+), 420 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java index 436a3e371..ae6bf5e94 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -23,7 +23,6 @@ import com.google.common.collect.ImmutableMap; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; - import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -36,7 +35,6 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,34 +45,36 @@ final class ConditionEvaluator { /** * Evaluates server conditions and assigns a boolean value to each condition. - * + * * @param conditions List of conditions which are to be evaluated. - * @param context A map with additional metadata used during evaluation. + * @param context A map with additional metadata used during evaluation. * @return A map of condition to evaluated value. */ @NonNull Map evaluateConditions( - @NonNull List conditions, - @Nullable KeysAndValues context) { + @NonNull List conditions, @Nullable KeysAndValues context) { checkNotNull(conditions, "List of conditions must not be null."); checkArgument(!conditions.isEmpty(), "List of conditions must not be empty."); if (context == null || conditions.isEmpty()) { return ImmutableMap.of(); } - KeysAndValues evaluationContext = context != null - ? context - : new KeysAndValues.Builder().build(); - - Map evaluatedConditions = conditions.stream() - .collect(ImmutableMap.toImmutableMap( - ServerCondition::getName, - condition -> evaluateCondition(condition.getCondition(), evaluationContext, /* nestingLevel= */0))); + KeysAndValues evaluationContext = + context != null ? context : new KeysAndValues.Builder().build(); + + Map evaluatedConditions = + conditions.stream() + .collect( + ImmutableMap.toImmutableMap( + ServerCondition::getName, + condition -> + evaluateCondition( + condition.getCondition(), evaluationContext, /* nestingLevel= */ 0))); return evaluatedConditions; } - private boolean evaluateCondition(OneOfCondition condition, KeysAndValues context, - int nestingLevel) { + private boolean evaluateCondition( + OneOfCondition condition, KeysAndValues context, int nestingLevel) { if (nestingLevel > MAX_CONDITION_RECURSION_DEPTH) { logger.warn("Maximum condition recursion depth exceeded."); return false; @@ -97,29 +97,30 @@ private boolean evaluateCondition(OneOfCondition condition, KeysAndValues contex return false; } - private boolean evaluateOrCondition(OrCondition condition, KeysAndValues context, - int nestingLevel) { + private boolean evaluateOrCondition( + OrCondition condition, KeysAndValues context, int nestingLevel) { return condition.getConditions().stream() .anyMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1)); } - private boolean evaluateAndCondition(AndCondition condition, KeysAndValues context, - int nestingLevel) { + private boolean evaluateAndCondition( + AndCondition condition, KeysAndValues context, int nestingLevel) { return condition.getConditions().stream() .allMatch(subCondition -> evaluateCondition(subCondition, context, nestingLevel + 1)); } - private boolean evaluateCustomSignalCondition(CustomSignalCondition condition, - KeysAndValues context) { + private boolean evaluateCustomSignalCondition( + CustomSignalCondition condition, KeysAndValues context) { CustomSignalOperator customSignalOperator = condition.getCustomSignalOperator(); String customSignalKey = condition.getCustomSignalKey(); - ImmutableList targetCustomSignalValues = ImmutableList.copyOf( - condition.getTargetCustomSignalValues()); + ImmutableList targetCustomSignalValues = + ImmutableList.copyOf(condition.getTargetCustomSignalValues()); if (targetCustomSignalValues.isEmpty()) { - logger.warn(String.format( - "Values must be assigned to all custom signal fields. Operator:%s, Key:%s, Values:%s", - customSignalOperator, customSignalKey, targetCustomSignalValues)); + logger.warn( + String.format( + "Values must be assigned to all custom signal fields. Operator:%s, Key:%s, Values:%s", + customSignalOperator, customSignalKey, targetCustomSignalValues)); return false; } @@ -131,64 +132,65 @@ private boolean evaluateCustomSignalCondition(CustomSignalCondition condition, switch (customSignalOperator) { // String operations. case STRING_CONTAINS: - return compareStrings(targetCustomSignalValues, customSignalValue, + return compareStrings( + targetCustomSignalValues, + customSignalValue, (customSignal, targetSignal) -> customSignal.contains(targetSignal)); case STRING_DOES_NOT_CONTAIN: - return !compareStrings(targetCustomSignalValues, customSignalValue, + return !compareStrings( + targetCustomSignalValues, + customSignalValue, (customSignal, targetSignal) -> customSignal.contains(targetSignal)); case STRING_EXACTLY_MATCHES: - return compareStrings(targetCustomSignalValues, customSignalValue, + return compareStrings( + targetCustomSignalValues, + customSignalValue, (customSignal, targetSignal) -> customSignal.equals(targetSignal)); case STRING_CONTAINS_REGEX: - return compareStrings(targetCustomSignalValues, customSignalValue, + return compareStrings( + targetCustomSignalValues, + customSignalValue, (customSignal, targetSignal) -> compareStringRegex(customSignal, targetSignal)); // Numeric operations. case NUMERIC_LESS_THAN: - return compareNumbers(targetCustomSignalValues, customSignalValue, - (result) -> result < 0); + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result < 0); case NUMERIC_LESS_EQUAL: - return compareNumbers(targetCustomSignalValues, customSignalValue, - (result) -> result <= 0); + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result <= 0); case NUMERIC_EQUAL: - return compareNumbers(targetCustomSignalValues, customSignalValue, - (result) -> result == 0); + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result == 0); case NUMERIC_NOT_EQUAL: - return compareNumbers(targetCustomSignalValues, customSignalValue, - (result) -> result != 0); + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result != 0); case NUMERIC_GREATER_THAN: - return compareNumbers(targetCustomSignalValues, customSignalValue, - (result) -> result > 0); + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result > 0); case NUMERIC_GREATER_EQUAL: - return compareNumbers(targetCustomSignalValues, customSignalValue, - (result) -> result >= 0); + return compareNumbers(targetCustomSignalValues, customSignalValue, (result) -> result >= 0); // Semantic operations. case SEMANTIC_VERSION_EQUAL: - return compareSemanticVersions(targetCustomSignalValues, customSignalValue, - (result) -> result == 0); + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result == 0); case SEMANTIC_VERSION_GREATER_EQUAL: - return compareSemanticVersions(targetCustomSignalValues, customSignalValue, - (result) -> result >= 0); + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result >= 0); case SEMANTIC_VERSION_GREATER_THAN: - return compareSemanticVersions(targetCustomSignalValues, customSignalValue, - (result) -> result > 0); + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result > 0); case SEMANTIC_VERSION_LESS_EQUAL: - return compareSemanticVersions(targetCustomSignalValues, customSignalValue, - (result) -> result <= 0); + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result <= 0); case SEMANTIC_VERSION_LESS_THAN: - return compareSemanticVersions(targetCustomSignalValues, customSignalValue, - (result) -> result < 0); + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result < 0); case SEMANTIC_VERSION_NOT_EQUAL: - return compareSemanticVersions(targetCustomSignalValues, customSignalValue, - (result) -> result != 0); + return compareSemanticVersions( + targetCustomSignalValues, customSignalValue, (result) -> result != 0); default: return false; } } - private boolean evaluatePercentCondition(PercentCondition condition, - KeysAndValues context) { + private boolean evaluatePercentCondition(PercentCondition condition, KeysAndValues context) { if (!context.containsKey("randomizationId")) { logger.warn("Percentage operation must not be performed without randomizationId"); return false; @@ -198,18 +200,16 @@ private boolean evaluatePercentCondition(PercentCondition condition, // The micro-percent interval to be used with the BETWEEN operator. MicroPercentRange microPercentRange = condition.getMicroPercentRange(); - int microPercentUpperBound = microPercentRange != null - ? microPercentRange.getMicroPercentUpperBound() - : 0; - int microPercentLowerBound = microPercentRange != null - ? microPercentRange.getMicroPercentLowerBound() - : 0; + int microPercentUpperBound = + microPercentRange != null ? microPercentRange.getMicroPercentUpperBound() : 0; + int microPercentLowerBound = + microPercentRange != null ? microPercentRange.getMicroPercentLowerBound() : 0; // The limit of percentiles to target in micro-percents when using the // LESS_OR_EQUAL and GREATER_THAN operators. The value must be in the range [0 // and 100000000]. int microPercent = condition.getMicroPercent(); - BigInteger microPercentile = getMicroPercentile(condition.getSeed(), - context.get("randomizationId")); + BigInteger microPercentile = + getMicroPercentile(condition.getSeed(), context.get("randomizationId")); switch (operator) { case LESS_OR_EQUAL: return microPercentile.compareTo(BigInteger.valueOf(microPercent)) <= 0; @@ -247,9 +247,12 @@ private BigInteger hashSeededRandomizationId(String seededRandomizationId) { } } - private boolean compareStrings(ImmutableList targetValues, String customSignal, + private boolean compareStrings( + ImmutableList targetValues, + String customSignal, BiPredicate compareFunction) { - return targetValues.stream().anyMatch(targetValue -> compareFunction.test(customSignal, targetValue)); + return targetValues.stream() + .anyMatch(targetValue -> compareFunction.test(customSignal, targetValue)); } private boolean compareStringRegex(String customSignal, String targetSignal) { @@ -260,12 +263,13 @@ private boolean compareStringRegex(String customSignal, String targetSignal) { } } - private boolean compareNumbers(ImmutableList targetValues, String customSignal, - IntPredicate compareFunction) { + private boolean compareNumbers( + ImmutableList targetValues, String customSignal, IntPredicate compareFunction) { if (targetValues.size() != 1) { - logger.warn(String.format( - "Target values must contain 1 element for numeric operations. Target Value: %s", - targetValues)); + logger.warn( + String.format( + "Target values must contain 1 element for numeric operations. Target Value: %s", + targetValues)); return false; } @@ -275,23 +279,22 @@ private boolean compareNumbers(ImmutableList targetValues, String custom int comparisonResult = Double.compare(customSignalDouble, targetValue); return compareFunction.test(comparisonResult); } catch (NumberFormatException e) { - logger.warn("Error parsing numeric values: customSignal=%s, targetValue=%s", + logger.warn( + "Error parsing numeric values: customSignal=%s, targetValue=%s", customSignal, targetValues.get(0), e); return false; } } - private boolean compareSemanticVersions(ImmutableList targetValues, - String customSignal, - IntPredicate compareFunction) { + private boolean compareSemanticVersions( + ImmutableList targetValues, String customSignal, IntPredicate compareFunction) { if (targetValues.size() != 1) { logger.warn(String.format("Target values must contain 1 element for semantic operation.")); return false; } String targetValueString = targetValues.get(0); - if (!validateSemanticVersion(targetValueString) - || !validateSemanticVersion(customSignal)) { + if (!validateSemanticVersion(targetValueString) || !validateSemanticVersion(customSignal)) { return false; } @@ -300,7 +303,8 @@ private boolean compareSemanticVersions(ImmutableList targetValues, int maxLength = 5; if (targetVersion.size() > maxLength || customSignalVersion.size() > maxLength) { - logger.warn("Semantic version max length(%s) exceeded. Target: %s, Custom Signal: %s", + logger.warn( + "Semantic version max length(%s) exceeded. Target: %s, Custom Signal: %s", maxLength, targetValueString, customSignal); return false; } diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index 4ccf906d8..78a5c276a 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -61,144 +61,156 @@ public class FirebaseRemoteConfigClientImplTest { - private static final String TEST_REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; - private static final String TEST_SERVER_REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + private static final String TEST_REMOTE_CONFIG_URL = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + private static final String TEST_SERVER_REMOTE_CONFIG_URL = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; private static final List HTTP_STATUS_CODES = ImmutableList.of(401, 404, 500); - private static final Map HTTP_STATUS_TO_ERROR_CODE = ImmutableMap.of( - 401, ErrorCode.UNAUTHENTICATED, - 404, ErrorCode.NOT_FOUND, - 500, ErrorCode.INTERNAL); + private static final Map HTTP_STATUS_TO_ERROR_CODE = + ImmutableMap.of( + 401, ErrorCode.UNAUTHENTICATED, + 404, ErrorCode.NOT_FOUND, + 500, ErrorCode.INTERNAL); - private static final String MOCK_TEMPLATE_RESPONSE = TestUtils - .loadResource("getRemoteConfig.json"); + private static final String MOCK_TEMPLATE_RESPONSE = + TestUtils.loadResource("getRemoteConfig.json"); - private static final String MOCK_SERVER_TEMPLATE_RESPONSE = TestUtils - .loadResource("getServerRemoteConfig.json"); + private static final String MOCK_SERVER_TEMPLATE_RESPONSE = + TestUtils.loadResource("getServerRemoteConfig.json"); - private static final String MOCK_LIST_VERSIONS_RESPONSE = TestUtils - .loadResource("listRemoteConfigVersions.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( - "welcome_message_text", - new Parameter() - .setDefaultValue(ParameterValue.of("welcome to app")) - .setConditionalValues( - ImmutableMap.of( - "ios_en", ParameterValue.of("welcome to app en"))) - .setDescription("text for welcome message!") - .setValueType(ParameterValueType.STRING), - "header_text", - new Parameter() - .setDefaultValue(ParameterValue.inAppDefault()) - .setValueType(ParameterValueType.STRING)); - - private static final Map EXPECTED_PARAMETER_GROUPS = ImmutableMap.of( - "new menu", - new ParameterGroup() - .setDescription("New Menu") - .setParameters( - ImmutableMap.of( - "pumpkin_spice_season", - new Parameter() - .setDefaultValue(ParameterValue.of("true")) - .setDescription("Whether it's currently pumpkin spice season.") - .setValueType(ParameterValueType.BOOLEAN)))); - - private static final List EXPECTED_CONDITIONS = ImmutableList.of( - new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']") - .setTagColor(TagColor.INDIGO), - new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']")); - - private static final List EXPECTED_SERVER_CONDITIONS = ImmutableList.of( - new ServerCondition("custom_signal", null) - .setServerCondition( - new OneOfCondition() - .setOrCondition( - new OrCondition( - ImmutableList.of( - new OneOfCondition() - .setAndCondition( - new AndCondition( - ImmutableList.of( - new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( - "users", - CustomSignalOperator.NUMERIC_LESS_THAN, - new ArrayList<>( - ImmutableList.of("100"))))))))))), - new ServerCondition("percent", null) - .setServerCondition( - new OneOfCondition() - .setOrCondition( - new OrCondition( - ImmutableList.of( - new OneOfCondition() - .setAndCondition( - new AndCondition( - ImmutableList.of( - new OneOfCondition() - .setPercent( - new PercentCondition( - new MicroPercentRange( - 12000000, 100000000), - PercentConditionOperator.BETWEEN, - "3maarirs9xzs"))))))))), - new ServerCondition("chained_conditions", null) - .setServerCondition( - new OneOfCondition() - .setOrCondition( - new OrCondition( - ImmutableList.of( - new OneOfCondition() - .setAndCondition( - new AndCondition( - ImmutableList.of( - new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( - "users", - CustomSignalOperator.NUMERIC_LESS_THAN, - new ArrayList<>( - ImmutableList.of("100")))), - new OneOfCondition() - .setCustomSignal( - new CustomSignalCondition( - "premium users", - CustomSignalOperator.NUMERIC_GREATER_THAN, - new ArrayList<>( - ImmutableList.of("20")))), - new OneOfCondition() - .setPercent( - new PercentCondition( - new MicroPercentRange( - 25000000, 100000000), - PercentConditionOperator.BETWEEN, - "cla24qoibb61")))))))))); - - private static final Version EXPECTED_VERSION = new Version( - new TemplateResponse.VersionResponse() - .setVersionNumber("17") - .setUpdateOrigin("ADMIN_SDK_NODE") - .setUpdateType("INCREMENTAL_UPDATE") - .setUpdateUser( - new TemplateResponse.UserResponse() - .setEmail("firebase-user@account.com") - .setName("dev-admin") - .setImageUrl("http://image.jpg")) - .setUpdateTime("2020-11-15T06:57:26.342763941Z") - .setDescription("promo config")); - - private static final Template EXPECTED_TEMPLATE = new Template() - .setETag(TEST_ETAG) - .setParameters(EXPECTED_PARAMETERS) - .setConditions(EXPECTED_CONDITIONS) - .setParameterGroups(EXPECTED_PARAMETER_GROUPS) - .setVersion(EXPECTED_VERSION); + private static final Map EXPECTED_PARAMETERS = + ImmutableMap.of( + "welcome_message_text", + new Parameter() + .setDefaultValue(ParameterValue.of("welcome to app")) + .setConditionalValues( + ImmutableMap.of( + "ios_en", ParameterValue.of("welcome to app en"))) + .setDescription("text for welcome message!") + .setValueType(ParameterValueType.STRING), + "header_text", + new Parameter() + .setDefaultValue(ParameterValue.inAppDefault()) + .setValueType(ParameterValueType.STRING)); + + private static final Map EXPECTED_PARAMETER_GROUPS = + ImmutableMap.of( + "new menu", + new ParameterGroup() + .setDescription("New Menu") + .setParameters( + ImmutableMap.of( + "pumpkin_spice_season", + new Parameter() + .setDefaultValue(ParameterValue.of("true")) + .setDescription("Whether it's currently pumpkin spice season.") + .setValueType(ParameterValueType.BOOLEAN)))); + + private static final List EXPECTED_CONDITIONS = + ImmutableList.of( + new Condition("ios_en", "device.os == 'ios' && device.country in ['us', 'uk']") + .setTagColor(TagColor.INDIGO), + new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']")); + + private static final List EXPECTED_SERVER_CONDITIONS = + ImmutableList.of( + new ServerCondition("custom_signal", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100"))))))))))), + new ServerCondition("percent", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange( + 12000000, 100000000), + PercentConditionOperator.BETWEEN, + "3maarirs9xzs"))))))))), + new ServerCondition("chained_conditions", null) + .setServerCondition( + new OneOfCondition() + .setOrCondition( + new OrCondition( + ImmutableList.of( + new OneOfCondition() + .setAndCondition( + new AndCondition( + ImmutableList.of( + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "users", + CustomSignalOperator + .NUMERIC_LESS_THAN, + new ArrayList<>( + ImmutableList.of("100")))), + new OneOfCondition() + .setCustomSignal( + new CustomSignalCondition( + "premium users", + CustomSignalOperator + .NUMERIC_GREATER_THAN, + new ArrayList<>( + ImmutableList.of("20")))), + new OneOfCondition() + .setPercent( + new PercentCondition( + new MicroPercentRange( + 25000000, 100000000), + PercentConditionOperator.BETWEEN, + "cla24qoibb61")))))))))); + + private static final Version EXPECTED_VERSION = + new Version( + new TemplateResponse.VersionResponse() + .setVersionNumber("17") + .setUpdateOrigin("ADMIN_SDK_NODE") + .setUpdateType("INCREMENTAL_UPDATE") + .setUpdateUser( + new TemplateResponse.UserResponse() + .setEmail("firebase-user@account.com") + .setName("dev-admin") + .setImageUrl("http://image.jpg")) + .setUpdateTime("2020-11-15T06:57:26.342763941Z") + .setDescription("promo config")); + + private static final Template EXPECTED_TEMPLATE = + new Template() + .setETag(TEST_ETAG) + .setParameters(EXPECTED_PARAMETERS) + .setConditions(EXPECTED_CONDITIONS) + .setParameterGroups(EXPECTED_PARAMETER_GROUPS) + .setVersion(EXPECTED_VERSION); private MockLowLevelHttpResponse response; private TestResponseInterceptor interceptor; @@ -228,18 +240,20 @@ public void testGetTemplate() throws Exception { @Test public void testGetTemplateWithTimestampUpToNanosecondPrecision() throws Exception { - List timestamps = ImmutableList.of( - "2020-11-15T06:57:26.342Z", - "2020-11-15T06:57:26.342763Z", - "2020-11-15T06:57:26.342763941Z"); + List timestamps = + ImmutableList.of( + "2020-11-15T06:57:26.342Z", + "2020-11-15T06:57:26.342763Z", + "2020-11-15T06:57:26.342763941Z"); for (String timestamp : timestamps) { response.addHeader("etag", TEST_ETAG); - String templateResponse = "{\"version\": {" - + " \"versionNumber\": \"17\"," - + " \"updateTime\": \"" - + timestamp - + "\"" - + " }}"; + String templateResponse = + "{\"version\": {" + + " \"versionNumber\": \"17\"," + + " \"updateTime\": \"" + + timestamp + + "\"" + + " }}"; response.setContent(templateResponse); Template receivedTemplate = client.getTemplate(); @@ -380,15 +394,17 @@ public void testGetTemplateErrorWithMalformedResponse() { @Test public void testGetTemplateErrorWithDetails() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); + response + .setStatusCode(code) + .setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", \"message\": \"test error\"}}"); try { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, null, "test error", - HttpMethods.GET); + checkExceptionFromHttpResponse( + error, ErrorCode.INVALID_ARGUMENT, null, "test error", HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); } @@ -397,16 +413,21 @@ public void testGetTemplateErrorWithDetails() { @Test public void testGetTemplateErrorWithRcError() { for (int code : HTTP_STATUS_CODES) { - response.setStatusCode(code).setContent( - "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " - + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); + response + .setStatusCode(code) + .setContent( + "{\"error\": {\"status\": \"INVALID_ARGUMENT\", " + + "\"message\": \"[INVALID_ARGUMENT]: test error\"}}"); try { client.getTemplate(); fail("No error thrown for HTTP error"); } catch (FirebaseRemoteConfigException error) { - checkExceptionFromHttpResponse(error, ErrorCode.INVALID_ARGUMENT, - RemoteConfigErrorCode.INVALID_ARGUMENT, "[INVALID_ARGUMENT]: test error", + checkExceptionFromHttpResponse( + error, + ErrorCode.INVALID_ARGUMENT, + RemoteConfigErrorCode.INVALID_ARGUMENT, + "[INVALID_ARGUMENT]: test error", HttpMethods.GET); } checkGetRequestHeader(interceptor.getLastRequest()); @@ -422,8 +443,9 @@ public void testGetTemplateAtVersionWithNullString() throws Exception { @Test public void testGetTemplateAtVersionWithInvalidString() throws Exception { - List invalidVersionStrings = ImmutableList.of( - "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = + ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -655,12 +677,13 @@ public void testPublishTemplateWithValidTemplateAndForceTrue() throws Exception public void testPublishTemplateWithValidTemplateAndValidateOnlyTrue() throws Exception { response.addHeader("etag", TEST_ETAG); response.setContent(MOCK_TEMPLATE_RESPONSE); - Template expectedTemplate = new Template() - .setETag("etag-123456789012-45") - .setParameters(EXPECTED_PARAMETERS) - .setConditions(EXPECTED_CONDITIONS) - .setParameterGroups(EXPECTED_PARAMETER_GROUPS) - .setVersion(EXPECTED_VERSION); + Template expectedTemplate = + new Template() + .setETag("etag-123456789012-45") + .setParameters(EXPECTED_PARAMETERS) + .setConditions(EXPECTED_CONDITIONS) + .setParameterGroups(EXPECTED_PARAMETER_GROUPS) + .setVersion(EXPECTED_VERSION); Template validatedTemplate = client.publishTemplate(expectedTemplate, true, false); @@ -851,8 +874,9 @@ public void testRollbackWithNullString() throws Exception { @Test public void testRollbackWithInvalidString() throws Exception { - List invalidVersionStrings = ImmutableList.of( - "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); + List invalidVersionStrings = + ImmutableList.of( + "", " ", "abc", "t123", "123t", "t123t", "12t3", "#$*&^", "-123", "+123", "123.4"); for (String version : invalidVersionStrings) { try { @@ -1081,26 +1105,28 @@ public void testListVersionsWithNullOptions() throws Exception { 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()); + 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"); + 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("&"); @@ -1287,10 +1313,11 @@ public void testBuilderNullRequestFactory() { @Test public void testFromApp() throws IOException { - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); + FirebaseOptions options = + FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); FirebaseApp app = FirebaseApp.initializeApp(options); try { @@ -1299,7 +1326,8 @@ public void testFromApp() throws IOException { assertEquals(TEST_REMOTE_CONFIG_URL, client.getRemoteConfigUrl()); assertSame(options.getJsonFactory(), client.getJsonFactory()); - HttpRequest request = client.getRequestFactory().buildGetRequest(new GenericUrl("https://example.com")); + HttpRequest request = + client.getRequestFactory().buildGetRequest(new GenericUrl("https://example.com")); assertEquals("Bearer test-token", request.getHeaders().getAuthorization()); } finally { app.delete(); @@ -1308,7 +1336,8 @@ public void testFromApp() throws IOException { private FirebaseRemoteConfigClientImpl initRemoteConfigClient( MockLowLevelHttpResponse mockResponse, HttpResponseInterceptor interceptor) { - MockHttpTransport transport = new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build(); + MockHttpTransport transport = + new MockHttpTransport.Builder().setLowLevelHttpResponse(mockResponse).build(); return FirebaseRemoteConfigClientImpl.builder() .setProjectId("test-project") @@ -1431,18 +1460,20 @@ public void testGetServerTemplate() throws Exception { @Test public void testGetServerTemplateWithTimestampUpToNanosecondPrecision() throws Exception { - List timestamps = ImmutableList.of( - "2020-11-15T06:57:26.342Z", - "2020-11-15T06:57:26.342763Z", - "2020-11-15T06:57:26.342763941Z"); + List timestamps = + ImmutableList.of( + "2020-11-15T06:57:26.342Z", + "2020-11-15T06:57:26.342763Z", + "2020-11-15T06:57:26.342763941Z"); for (String timestamp : timestamps) { response.addHeader("etag", TEST_ETAG); - String templateResponse = "{\"version\": {" - + " \"versionNumber\": \"17\"," - + " \"updateTime\": \"" - + timestamp - + "\"" - + " }}"; + String templateResponse = + "{\"version\": {" + + " \"versionNumber\": \"17\"," + + " \"updateTime\": \"" + + timestamp + + "\"" + + " }}"; response.setContent(templateResponse); String receivedTemplate = client.getServerTemplate(); diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index e10ffe2a6..81124f38f 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -34,18 +34,16 @@ import org.junit.After; import org.junit.Test; -/** - * Unit tests - * for {@link FirebaseRemoteConfig}. - */ +/** Unit tests for {@link FirebaseRemoteConfig}. */ public class FirebaseRemoteConfigTest { - private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - private static final FirebaseRemoteConfigException TEST_EXCEPTION = new FirebaseRemoteConfigException( - ErrorCode.INTERNAL, "Test error message"); + private static final FirebaseOptions TEST_OPTIONS = + FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final FirebaseRemoteConfigException TEST_EXCEPTION = + new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); @After public void tearDown() { @@ -79,7 +77,8 @@ public void testDefaultRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } @@ -92,7 +91,8 @@ public void testDefaultServerRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getServerRemoteConfigUrl()); } @@ -114,17 +114,19 @@ public void testAppDelete() { @Test public void testRemoteConfigClientWithoutProjectId() { - FirebaseOptions options = FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); + FirebaseOptions options = + FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); FirebaseApp.initializeApp(options); try { FirebaseRemoteConfig.getInstance(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { - String message = "Project ID is required to access Remote Config service. Use a service " - + "account credential or set the project ID explicitly via FirebaseOptions. " - + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " - + "environment variable."; + String message = + "Project ID is required to access Remote Config service. Use a service " + + "account credential or set the project ID explicitly via FirebaseOptions. " + + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " + + "environment variable."; assertEquals(message, expected.getMessage()); } } @@ -135,7 +137,8 @@ public void testRemoteConfigClientWithoutProjectId() { @Test public void testGetTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplate(); @@ -157,7 +160,8 @@ public void testGetTemplateFailure() { @Test public void testGetTemplateAsync() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAsync().get(); @@ -181,7 +185,8 @@ public void testGetTemplateAsyncFailure() throws InterruptedException { @Test public void testGetTemplateAtVersionWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion("64"); @@ -203,7 +208,8 @@ public void testGetTemplateAtVersionWithStringValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync("55").get(); @@ -225,7 +231,8 @@ public void testGetTemplateAtVersionAsyncWithStringValueFailure() throws Interru @Test public void testGetTemplateAtVersionWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion(64L); @@ -247,7 +254,8 @@ public void testGetTemplateAtVersionWithLongValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync(55L).get(); @@ -415,7 +423,8 @@ public void testForcePublishTemplateAsyncFailure() throws InterruptedException { @Test public void testRollbackWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback("64"); @@ -437,7 +446,8 @@ public void testRollbackWithStringValueFailure() { @Test public void testRollbackAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync("55").get(); @@ -459,7 +469,8 @@ public void testRollbackAsyncWithStringValueFailure() throws InterruptedExceptio @Test public void testRollbackWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback(64L); @@ -481,7 +492,8 @@ public void testRollbackWithLongValueFailure() { @Test public void testRollbackAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync(55L).get(); @@ -505,8 +517,9 @@ public void testRollbackAsyncWithLongValueFailure() throws InterruptedException @Test public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersions(); @@ -528,8 +541,9 @@ public void testListVersionsWithNoOptionsFailure() { @Test public void testListVersionsAsyncWithNoOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync().get(); @@ -551,11 +565,13 @@ public void testListVersionsAsyncWithNoOptionsFailure() throws InterruptedExcept @Test public void testListVersionsWithOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersions(ListVersionsOptions.builder().build()); + ListVersionsPage listVersionsPage = + remoteConfig.listVersions(ListVersionsOptions.builder().build()); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -574,11 +590,13 @@ public void testListVersionsWithOptionsFailure() { @Test public void testListVersionsAsyncWithOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); + ListVersionsPage listVersionsPage = + remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -604,13 +622,15 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) @Test public void testGetServerTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplate(); String templateData = template.toJson(); - JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement expectedJson = + JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); JsonElement actualJson = JsonParser.parseString(templateData); assertEquals(expectedJson, actualJson); @@ -630,13 +650,15 @@ public void testGetServerTemplateFailure() { @Test public void testGetServerTemplateAsync() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplateAsync().get(); String templateData = template.toJson(); - JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement expectedJson = + JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); JsonElement actualJson = JsonParser.parseString(templateData); assertEquals(expectedJson, actualJson); diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java index e49de56c1..82da10732 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -24,20 +24,19 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.testing.TestUtils; -import org.junit.BeforeClass; -import org.junit.Test; import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import org.junit.BeforeClass; +import org.junit.Test; -/** - * Tests for {@link ServerTemplateImpl}. - */ +/** Tests for {@link ServerTemplateImpl}. */ public class ServerTemplateImplTest { - private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); + private static final FirebaseOptions TEST_OPTIONS = + FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); private static String cacheTemplate; @@ -51,20 +50,22 @@ public void testServerTemplateWithoutCacheValueThrowsException() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - IllegalArgumentException error = assertThrows(IllegalArgumentException.class, - () -> new ServerTemplateImpl.Builder(null).defaultConfig(defaultConfig).build()); + IllegalArgumentException error = + assertThrows( + IllegalArgumentException.class, + () -> new ServerTemplateImpl.Builder(null).defaultConfig(defaultConfig).build()); assertEquals("JSON String must not be null or empty.", error.getMessage()); } @Test - public void testEvaluateWithoutContextReturnsDefaultValue() - throws FirebaseRemoteConfigException { + public void testEvaluateWithoutContextReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(); @@ -75,10 +76,11 @@ public void testEvaluateWithoutContextReturnsDefaultValue() public void testEvaluateCustomSignalReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "100").build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -90,10 +92,11 @@ public void testEvaluateCustomSignalReturnsConditionalValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -105,10 +108,11 @@ public void testEvaluateCustomSignalWithoutContextReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -120,10 +124,11 @@ public void testEvaluateCustomSignalWithInvalidContextReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "abc").build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -135,10 +140,11 @@ public void testEvaluatePercentWithoutRandomizationIdReturnsDefaultValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -149,10 +155,11 @@ public void testEvaluatePercentWithoutRandomizationIdReturnsDefaultValue() public void testEvaluatePercentReturnsConditionalValue() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -164,10 +171,11 @@ public void testEvaluateWithoutDefaultValueReturnsEmptyString() throws FirebaseRemoteConfigException { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -180,26 +188,29 @@ public void testEvaluateWithInvalidCacheValueThrowsException() KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); String invalidJsonString = "abc"; - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(invalidJsonString) - .build(); - - FirebaseRemoteConfigException error = assertThrows(FirebaseRemoteConfigException.class, - () -> template.evaluate(context)); - - assertEquals("No Remote Config Server template in cache. Call load() before " - + "calling evaluate().", error.getMessage()); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(invalidJsonString) + .build(); + + FirebaseRemoteConfigException error = + assertThrows(FirebaseRemoteConfigException.class, () -> template.evaluate(context)); + + assertEquals( + "No Remote Config Server template in cache. Call load() before " + "calling evaluate().", + error.getMessage()); } @Test public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -210,10 +221,11 @@ public void testEvaluateWithInAppDefaultReturnsEmptyString() throws Exception { public void testEvaluateWithDerivedInAppDefaultReturnsDefaultValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -224,10 +236,11 @@ public void testEvaluateWithDerivedInAppDefaultReturnsDefaultValue() throws Exce public void testEvaluateWithMultipleConditionReturnsConditionalValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("users", "99").build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -237,14 +250,17 @@ public void testEvaluateWithMultipleConditionReturnsConditionalValue() throws Ex @Test public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - KeysAndValues context = new KeysAndValues.Builder().put("users", "100") - .put("premium users", 20) - .put("randomizationId", "user") - .build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + KeysAndValues context = + new KeysAndValues.Builder() + .put("users", "100") + .put("premium users", 20) + .put("randomizationId", "user") + .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -254,11 +270,13 @@ public void testEvaluateWithChainedAndConditionReturnsDefaultValue() throws Exce @Test public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); - KeysAndValues context = new KeysAndValues.Builder().put("users", "99").put("premium users", "30").build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + KeysAndValues context = + new KeysAndValues.Builder().put("users", "99").put("premium users", "30").build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -269,10 +287,11 @@ public void testEvaluateWithChainedAndConditionReturnsConditionalValue() throws public void testGetEvaluateConfigOnInvalidTypeReturnsDefaultValue() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().put("randomizationId", "user").build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -283,10 +302,11 @@ public void testGetEvaluateConfigOnInvalidTypeReturnsDefaultValue() throws Excep public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -297,10 +317,11 @@ public void testGetEvaluateConfigInvalidKeyReturnsStaticValueSource() throws Exc public void testGetEvaluateConfigInAppDefaultConfigReturnsDefaultValueSource() throws Exception { KeysAndValues defaultConfig = new KeysAndValues.Builder().put("In-app default", "abc").build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -309,12 +330,14 @@ public void testGetEvaluateConfigInAppDefaultConfigReturnsDefaultValueSource() t @Test public void testGetEvaluateConfigUnsetDefaultConfigReturnsDefaultValueSource() throws Exception { - KeysAndValues defaultConfig = new KeysAndValues.Builder().put("Unset default config", "abc").build(); + KeysAndValues defaultConfig = + new KeysAndValues.Builder().put("Unset default config", "abc").build(); KeysAndValues context = new KeysAndValues.Builder().build(); - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null) + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) + .build(); ServerConfig evaluatedConfig = template.evaluate(context); @@ -332,21 +355,25 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) public void testLoad() throws Exception { // 1. Define the template data that the mock client will return. // This is the EXPECTED state after `load()` is called. - final String expectedTemplateJsonAfterLoad = new ServerTemplateData().setETag(TEST_ETAG).toJSON(); + final String expectedTemplateJsonAfterLoad = + new ServerTemplateData().setETag(TEST_ETAG).toJSON(); // 2. Mock the HTTP client to return the predefined response. - MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate(expectedTemplateJsonAfterLoad); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate(expectedTemplateJsonAfterLoad); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); // 3. Build the template instance. // It's initialized with a complex `cacheTemplate` to ensure `load()` properly // overwrites it. - KeysAndValues defaultConfig = new KeysAndValues.Builder().put("Unset default config", "abc").build(); - ServerTemplate template = remoteConfig - .serverTemplateBuilder() - .defaultConfig(defaultConfig) - .cachedTemplate(cacheTemplate) // This is the initial state before `load()` - .build(); + KeysAndValues defaultConfig = + new KeysAndValues.Builder().put("Unset default config", "abc").build(); + ServerTemplate template = + remoteConfig + .serverTemplateBuilder() + .defaultConfig(defaultConfig) + .cachedTemplate(cacheTemplate) // This is the initial state before `load()` + .build(); // 4. Call the load method, which fetches the new template from the mock client. ApiFuture loadFuture = template.load(); @@ -376,9 +403,8 @@ public void testBuilderParsesCachedTemplateCorrectly() throws FirebaseRemoteConf // Act: // 2. Build a ServerTemplate instance from the original cached JSON string, // which triggers the parsing logic we want to test. - ServerTemplate template = new ServerTemplateImpl.Builder(null) - .cachedTemplate(cacheTemplate) - .build(); + ServerTemplate template = + new ServerTemplateImpl.Builder(null).cachedTemplate(cacheTemplate).build(); // Assert: // 3. Compare the JSON from the newly built template against the canonical From 9fabd60b58f80c7f136d3c89f376aa74a77e790a Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 23:02:51 +0530 Subject: [PATCH 21/29] fix multi line indent --- .../FirebaseRemoteConfigTest.java | 108 +++++++----------- .../remoteconfig/ServerTemplateImplTest.java | 4 +- 2 files changed, 46 insertions(+), 66 deletions(-) diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 81124f38f..5c1f133f7 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -34,16 +34,18 @@ import org.junit.After; import org.junit.Test; -/** Unit tests for {@link FirebaseRemoteConfig}. */ + +/** Tests +* for {@link FirebaseRemoteConfig}. +* */ public class FirebaseRemoteConfigTest { - private static final FirebaseOptions TEST_OPTIONS = - FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - private static final FirebaseRemoteConfigException TEST_EXCEPTION = - new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); + private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final FirebaseRemoteConfigException TEST_EXCEPTION = new FirebaseRemoteConfigException( + ErrorCode.INTERNAL, "Test error message"); @After public void tearDown() { @@ -77,8 +79,7 @@ public void testDefaultRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } @@ -91,8 +92,7 @@ public void testDefaultServerRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = - "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getServerRemoteConfigUrl()); } @@ -114,19 +114,17 @@ public void testAppDelete() { @Test public void testRemoteConfigClientWithoutProjectId() { - FirebaseOptions options = - FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); + FirebaseOptions options = FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); FirebaseApp.initializeApp(options); try { FirebaseRemoteConfig.getInstance(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { - String message = - "Project ID is required to access Remote Config service. Use a service " - + "account credential or set the project ID explicitly via FirebaseOptions. " - + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " - + "environment variable."; + String message = "Project ID is required to access Remote Config service. Use a service " + + "account credential or set the project ID explicitly via FirebaseOptions. " + + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " + + "environment variable."; assertEquals(message, expected.getMessage()); } } @@ -137,8 +135,7 @@ public void testRemoteConfigClientWithoutProjectId() { @Test public void testGetTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplate(); @@ -160,8 +157,7 @@ public void testGetTemplateFailure() { @Test public void testGetTemplateAsync() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAsync().get(); @@ -185,8 +181,7 @@ public void testGetTemplateAsyncFailure() throws InterruptedException { @Test public void testGetTemplateAtVersionWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion("64"); @@ -208,8 +203,7 @@ public void testGetTemplateAtVersionWithStringValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync("55").get(); @@ -231,8 +225,7 @@ public void testGetTemplateAtVersionAsyncWithStringValueFailure() throws Interru @Test public void testGetTemplateAtVersionWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion(64L); @@ -254,8 +247,7 @@ public void testGetTemplateAtVersionWithLongValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync(55L).get(); @@ -423,8 +415,7 @@ public void testForcePublishTemplateAsyncFailure() throws InterruptedException { @Test public void testRollbackWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback("64"); @@ -446,8 +437,7 @@ public void testRollbackWithStringValueFailure() { @Test public void testRollbackAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync("55").get(); @@ -469,8 +459,7 @@ public void testRollbackAsyncWithStringValueFailure() throws InterruptedExceptio @Test public void testRollbackWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback(64L); @@ -492,8 +481,7 @@ public void testRollbackWithLongValueFailure() { @Test public void testRollbackAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync(55L).get(); @@ -517,9 +505,8 @@ public void testRollbackAsyncWithLongValueFailure() throws InterruptedException @Test public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersions(); @@ -541,9 +528,8 @@ public void testListVersionsWithNoOptionsFailure() { @Test public void testListVersionsAsyncWithNoOptions() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync().get(); @@ -565,13 +551,11 @@ public void testListVersionsAsyncWithNoOptionsFailure() throws InterruptedExcept @Test public void testListVersionsWithOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = - remoteConfig.listVersions(ListVersionsOptions.builder().build()); + ListVersionsPage listVersionsPage = remoteConfig.listVersions(ListVersionsOptions.builder().build()); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -590,13 +574,11 @@ public void testListVersionsWithOptionsFailure() { @Test public void testListVersionsAsyncWithOptions() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = - remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); + ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -622,15 +604,13 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) @Test public void testGetServerTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplate(); String templateData = template.toJson(); - JsonElement expectedJson = - JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); JsonElement actualJson = JsonParser.parseString(templateData); assertEquals(expectedJson, actualJson); @@ -650,15 +630,13 @@ public void testGetServerTemplateFailure() { @Test public void testGetServerTemplateAsync() throws Exception { - MockRemoteConfigClient client = - MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplateAsync().get(); String templateData = template.toJson(); - JsonElement expectedJson = - JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); JsonElement actualJson = JsonParser.parseString(templateData); assertEquals(expectedJson, actualJson); diff --git a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java index 82da10732..bafe84331 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ServerTemplateImplTest.java @@ -29,7 +29,9 @@ import org.junit.BeforeClass; import org.junit.Test; -/** Tests for {@link ServerTemplateImpl}. */ +/** Tests +* for {@link ServerTemplateImpl}. +* */ public class ServerTemplateImplTest { private static final FirebaseOptions TEST_OPTIONS = From db3fc82e0da1b23289aa74d71faaba6c8b596bce Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Mon, 18 Aug 2025 23:07:41 +0530 Subject: [PATCH 22/29] fix multi line indents --- .../FirebaseRemoteConfigTest.java | 103 +++++++++++------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 5c1f133f7..8b416b67b 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -40,12 +40,13 @@ * */ public class FirebaseRemoteConfigTest { - private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() - .setCredentials(new MockGoogleCredentials("test-token")) - .setProjectId("test-project") - .build(); - private static final FirebaseRemoteConfigException TEST_EXCEPTION = new FirebaseRemoteConfigException( - ErrorCode.INTERNAL, "Test error message"); + private static final FirebaseOptions TEST_OPTIONS = + FirebaseOptions.builder() + .setCredentials(new MockGoogleCredentials("test-token")) + .setProjectId("test-project") + .build(); + private static final FirebaseRemoteConfigException TEST_EXCEPTION = + new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); @After public void tearDown() { @@ -79,7 +80,8 @@ public void testDefaultRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } @@ -92,7 +94,8 @@ public void testDefaultServerRemoteConfigClient() { assertTrue(client instanceof FirebaseRemoteConfigClientImpl); assertSame(client, remoteConfig.getRemoteConfigClient()); - String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getServerRemoteConfigUrl()); } @@ -114,17 +117,19 @@ public void testAppDelete() { @Test public void testRemoteConfigClientWithoutProjectId() { - FirebaseOptions options = FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); + FirebaseOptions options = + FirebaseOptions.builder().setCredentials(new MockGoogleCredentials("test-token")).build(); FirebaseApp.initializeApp(options); try { FirebaseRemoteConfig.getInstance(); fail("No error thrown for missing project ID"); } catch (IllegalArgumentException expected) { - String message = "Project ID is required to access Remote Config service. Use a service " - + "account credential or set the project ID explicitly via FirebaseOptions. " - + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " - + "environment variable."; + String message = + "Project ID is required to access Remote Config service. Use a service " + + "account credential or set the project ID explicitly via FirebaseOptions. " + + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT " + + "environment variable."; assertEquals(message, expected.getMessage()); } } @@ -135,7 +140,8 @@ public void testRemoteConfigClientWithoutProjectId() { @Test public void testGetTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplate(); @@ -157,7 +163,8 @@ public void testGetTemplateFailure() { @Test public void testGetTemplateAsync() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAsync().get(); @@ -181,7 +188,8 @@ public void testGetTemplateAsyncFailure() throws InterruptedException { @Test public void testGetTemplateAtVersionWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion("64"); @@ -203,7 +211,8 @@ public void testGetTemplateAtVersionWithStringValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync("55").get(); @@ -225,7 +234,8 @@ public void testGetTemplateAtVersionAsyncWithStringValueFailure() throws Interru @Test public void testGetTemplateAtVersionWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersion(64L); @@ -247,7 +257,8 @@ public void testGetTemplateAtVersionWithLongValueFailure() { @Test public void testGetTemplateAtVersionAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.getTemplateAtVersionAsync(55L).get(); @@ -415,7 +426,8 @@ public void testForcePublishTemplateAsyncFailure() throws InterruptedException { @Test public void testRollbackWithStringValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback("64"); @@ -437,7 +449,8 @@ public void testRollbackWithStringValueFailure() { @Test public void testRollbackAsyncWithStringValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync("55").get(); @@ -459,7 +472,8 @@ public void testRollbackAsyncWithStringValueFailure() throws InterruptedExceptio @Test public void testRollbackWithLongValue() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollback(64L); @@ -481,7 +495,8 @@ public void testRollbackWithLongValueFailure() { @Test public void testRollbackAsyncWithLongValue() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromTemplate(new Template().setETag(TEST_ETAG)); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); Template template = remoteConfig.rollbackAsync(55L).get(); @@ -505,8 +520,9 @@ public void testRollbackAsyncWithLongValueFailure() throws InterruptedException @Test public void testListVersionsWithNoOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersions(); @@ -528,8 +544,9 @@ public void testListVersionsWithNoOptionsFailure() { @Test public void testListVersionsAsyncWithNoOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync().get(); @@ -551,11 +568,13 @@ public void testListVersionsAsyncWithNoOptionsFailure() throws InterruptedExcept @Test public void testListVersionsWithOptions() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersions(ListVersionsOptions.builder().build()); + ListVersionsPage listVersionsPage = + remoteConfig.listVersions(ListVersionsOptions.builder().build()); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -574,11 +593,13 @@ public void testListVersionsWithOptionsFailure() { @Test public void testListVersionsAsyncWithOptions() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromListVersionsResponse( - new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromListVersionsResponse( + new TemplateResponse.ListVersionsResponse().setNextPageToken("token")); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); - ListVersionsPage listVersionsPage = remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); + ListVersionsPage listVersionsPage = + remoteConfig.listVersionsAsync(ListVersionsOptions.builder().build()).get(); assertEquals("token", listVersionsPage.getNextPageToken()); } @@ -604,13 +625,15 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) @Test public void testGetServerTemplate() throws FirebaseRemoteConfigException { - MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplate(); String templateData = template.toJson(); - JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement expectedJson = + JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); JsonElement actualJson = JsonParser.parseString(templateData); assertEquals(expectedJson, actualJson); @@ -630,13 +653,15 @@ public void testGetServerTemplateFailure() { @Test public void testGetServerTemplateAsync() throws Exception { - MockRemoteConfigClient client = MockRemoteConfigClient.fromServerTemplate( - new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); ServerTemplate template = remoteConfig.getServerTemplateAsync().get(); String templateData = template.toJson(); - JsonElement expectedJson = JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + JsonElement expectedJson = + JsonParser.parseString(new ServerTemplateData().setETag(TEST_ETAG).toJSON()); JsonElement actualJson = JsonParser.parseString(templateData); assertEquals(expectedJson, actualJson); From 4dd8b1df235aff6793e33c82c6f9f927bcf74382 Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:12:30 +0530 Subject: [PATCH 23/29] Update ConditionEvaluator.java --- .../com/google/firebase/remoteconfig/ConditionEvaluator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java index ae6bf5e94..a819c9831 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import java.math.BigInteger; @@ -64,7 +65,7 @@ Map evaluateConditions( Map evaluatedConditions = conditions.stream() .collect( - ImmutableMap.toImmutableMap( + toImmutableMap( ServerCondition::getName, condition -> evaluateCondition( From 1a8006b4dc97f974aa926aac618a67ec2393a4c9 Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:16:31 +0530 Subject: [PATCH 24/29] Update ConditionEvaluator.java --- .../com/google/firebase/remoteconfig/ConditionEvaluator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java index a819c9831..8361a143e 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -18,10 +18,10 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import java.math.BigInteger; From 29a4b36344311a83c8c87d48f6ce8b765eb6ac29 Mon Sep 17 00:00:00 2001 From: Athira M Date: Fri, 29 Aug 2025 17:52:07 -0700 Subject: [PATCH 25/29] Restore deleted test file --- .../remoteconfig/ParameterValueTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java new file mode 100644 index 000000000..95ce424d6 --- /dev/null +++ b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java @@ -0,0 +1,53 @@ +/* + * 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.assertNotEquals; + +import org.junit.Test; + +public class ParameterValueTest { + + @Test + public void testCreateExplicitValue() { + final ParameterValue.Explicit parameterValue = ParameterValue.of("title text"); + + assertEquals("title text", parameterValue.getValue()); + } + + @Test + public void testCreateInAppDefault() { + final ParameterValue.InAppDefault parameterValue = ParameterValue.inAppDefault(); + + assertEquals(ParameterValue.InAppDefault.class, parameterValue.getClass()); + } + + @Test + public void testEquality() { + ParameterValue.Explicit parameterValueOne = ParameterValue.of("value"); + ParameterValue.Explicit parameterValueTwo = ParameterValue.of("value"); + ParameterValue.Explicit parameterValueThree = ParameterValue.of("title"); + + assertEquals(parameterValueOne, parameterValueTwo); + assertNotEquals(parameterValueOne, parameterValueThree); + + ParameterValue.InAppDefault parameterValueFour = ParameterValue.inAppDefault(); + ParameterValue.InAppDefault parameterValueFive = ParameterValue.inAppDefault(); + + assertEquals(parameterValueFour, parameterValueFive); + } +} From 5df02dec7d14676614e1794c5c7452f29ac930ab Mon Sep 17 00:00:00 2001 From: Athira M Date: Fri, 29 Aug 2025 17:56:23 -0700 Subject: [PATCH 26/29] restore deleted file --- .../remoteconfig/ParameterValueTest.java | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java index 95ce424d6..842fd808f 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java @@ -13,6 +13,7 @@ * 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; @@ -22,32 +23,32 @@ public class ParameterValueTest { - @Test - public void testCreateExplicitValue() { - final ParameterValue.Explicit parameterValue = ParameterValue.of("title text"); + @Test + public void testCreateExplicitValue() { + final ParameterValue.Explicit parameterValue = ParameterValue.of("title text"); - assertEquals("title text", parameterValue.getValue()); - } + assertEquals("title text", parameterValue.getValue()); + } - @Test - public void testCreateInAppDefault() { - final ParameterValue.InAppDefault parameterValue = ParameterValue.inAppDefault(); + @Test + public void testCreateInAppDefault() { + final ParameterValue.InAppDefault parameterValue = ParameterValue.inAppDefault(); - assertEquals(ParameterValue.InAppDefault.class, parameterValue.getClass()); - } + assertEquals(ParameterValue.InAppDefault.class, parameterValue.getClass()); + } - @Test - public void testEquality() { - ParameterValue.Explicit parameterValueOne = ParameterValue.of("value"); - ParameterValue.Explicit parameterValueTwo = ParameterValue.of("value"); - ParameterValue.Explicit parameterValueThree = ParameterValue.of("title"); + @Test + public void testEquality() { + ParameterValue.Explicit parameterValueOne = ParameterValue.of("value"); + ParameterValue.Explicit parameterValueTwo = ParameterValue.of("value"); + ParameterValue.Explicit parameterValueThree = ParameterValue.of("title"); - assertEquals(parameterValueOne, parameterValueTwo); - assertNotEquals(parameterValueOne, parameterValueThree); + assertEquals(parameterValueOne, parameterValueTwo); + assertNotEquals(parameterValueOne, parameterValueThree); - ParameterValue.InAppDefault parameterValueFour = ParameterValue.inAppDefault(); - ParameterValue.InAppDefault parameterValueFive = ParameterValue.inAppDefault(); + ParameterValue.InAppDefault parameterValueFour = ParameterValue.inAppDefault(); + ParameterValue.InAppDefault parameterValueFive = ParameterValue.inAppDefault(); - assertEquals(parameterValueFour, parameterValueFive); - } + assertEquals(parameterValueFour, parameterValueFive); + } } From 5325712bf3d6a9fdb94e7337196eed50f60e1377 Mon Sep 17 00:00:00 2001 From: Athira M Date: Mon, 8 Sep 2025 18:01:27 -0700 Subject: [PATCH 27/29] Return empty list if conditions are empty rather than throwing error --- .../com/google/firebase/remoteconfig/ConditionEvaluator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java index 8361a143e..ca7486dcd 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java +++ b/src/main/java/com/google/firebase/remoteconfig/ConditionEvaluator.java @@ -55,7 +55,6 @@ final class ConditionEvaluator { Map evaluateConditions( @NonNull List conditions, @Nullable KeysAndValues context) { checkNotNull(conditions, "List of conditions must not be null."); - checkArgument(!conditions.isEmpty(), "List of conditions must not be empty."); if (context == null || conditions.isEmpty()) { return ImmutableMap.of(); } From 43ed836a82ff152cff091fd9e03b9af643eac516 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Tue, 9 Sep 2025 08:16:30 +0530 Subject: [PATCH 28/29] Fix Server Condition --- .../remoteconfig/internal/ServerTemplateResponse.java | 4 ++-- src/test/resources/getServerRemoteConfig.json | 8 ++++---- src/test/resources/getServerTemplateData.json | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java index c58faa94b..82c3c9dd3 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java @@ -33,7 +33,7 @@ public final class ServerTemplateResponse { @Key("parameters") private Map parameters; - @Key("serverConditions") + @Key("conditions") private List serverConditions; @Key("parameterGroups") @@ -104,7 +104,7 @@ public static final class ServerConditionResponse { @Key("name") private String name; - @Key("serverCondition") + @Key("condition") private OneOfConditionResponse condition; public String getName() { diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json index 19967e958..bbbcb1b44 100644 --- a/src/test/resources/getServerRemoteConfig.json +++ b/src/test/resources/getServerRemoteConfig.json @@ -1,8 +1,8 @@ { - "serverConditions": [ + "conditions": [ { "name": "custom_signal", - "serverCondition": { + "condition": { "orCondition": { "conditions": [ { @@ -26,7 +26,7 @@ }, { "name": "percent", - "serverCondition": { + "condition": { "orCondition": { "conditions": [ { @@ -51,7 +51,7 @@ }, { "name": "chained_conditions", - "serverCondition": { + "condition": { "orCondition": { "conditions": [ { diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json index 91e3971e3..7a1df860c 100644 --- a/src/test/resources/getServerTemplateData.json +++ b/src/test/resources/getServerTemplateData.json @@ -1,8 +1,8 @@ { - "serverConditions": [ + "conditions": [ { "name": "custom_signal", - "serverCondition": { + "condition": { "orCondition": { "conditions": [ { @@ -26,7 +26,7 @@ }, { "name": "percent", - "serverCondition": { + "condition": { "orCondition": { "conditions": [ { @@ -51,7 +51,7 @@ }, { "name": "chained_conditions", - "serverCondition": { + "condition": { "orCondition": { "conditions": [ { From 9691434f5b560d2b048ed0691e01c8c2787c4910 Mon Sep 17 00:00:00 2001 From: Varun Rathore Date: Tue, 9 Sep 2025 08:36:11 +0530 Subject: [PATCH 29/29] fix percent condition --- .../remoteconfig/internal/ServerTemplateResponse.java | 2 +- src/test/resources/getServerRemoteConfig.json | 4 ++-- src/test/resources/getServerTemplateData.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java index 82c3c9dd3..db3785afe 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java @@ -250,7 +250,7 @@ public static final class PercentConditionResponse { @Key("microPercentRange") private MicroPercentRangeResponse microPercentRange; - @Key("percentConditionOperator") + @Key("percentOperator") private String percentOperator; @Key("seed") diff --git a/src/test/resources/getServerRemoteConfig.json b/src/test/resources/getServerRemoteConfig.json index bbbcb1b44..e5cb1a9eb 100644 --- a/src/test/resources/getServerRemoteConfig.json +++ b/src/test/resources/getServerRemoteConfig.json @@ -34,7 +34,7 @@ "conditions": [ { "percent": { - "percentConditionOperator": "BETWEEN", + "percentOperator": "BETWEEN", "seed": "3maarirs9xzs", "microPercentRange": { "microPercentLowerBound": 12000000, @@ -77,7 +77,7 @@ }, { "percent": { - "percentConditionOperator": "BETWEEN", + "percentOperator": "BETWEEN", "seed": "cla24qoibb61", "microPercentRange": { "microPercentLowerBound": 25000000, diff --git a/src/test/resources/getServerTemplateData.json b/src/test/resources/getServerTemplateData.json index 7a1df860c..c26f74185 100644 --- a/src/test/resources/getServerTemplateData.json +++ b/src/test/resources/getServerTemplateData.json @@ -34,7 +34,7 @@ "conditions": [ { "percent": { - "percentConditionOperator": "BETWEEN", + "percentOperator": "BETWEEN", "seed": "3maarirs9xzs", "microPercentRange": { "microPercentLowerBound": 12000000, @@ -75,7 +75,7 @@ ] }, "percent": { - "percentConditionOperator": "BETWEEN", + "percentOperator": "BETWEEN", "seed": "cla24qoibb61", "microPercentRange": { "microPercentLowerBound": 25000000,