Skip to content

Commit

Permalink
Feature/add error handler for callback (#31)
Browse files Browse the repository at this point in the history
* Update GBFeaturesRepository.java

Extension refreshCallback

* Update FeatureRefreshCallback.java

Extension refresh callback

* Update lib/src/main/java/growthbook/sdk/java/GBFeaturesRepository.java

Co-authored-by: Tina Holly <113377031+tinahollygb@users.noreply.github.com>

* fix comments by pull request

* add test for check refresh callbacks

* correct test

* correct test

* remove unnecessary space in test

* remove unnecessary space in test

* remove unnecessary space in test

* correct test after mr

---------

Co-authored-by: Tina Holly <113377031+tinahollygb@users.noreply.github.com>
Co-authored-by: ASalakhov <asalakhov@sportmasterlab.net>
  • Loading branch information
3 people committed Aug 2, 2023
1 parent 8dae94d commit eaaa583
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ public interface FeatureRefreshCallback {
* @param featuresJson Features as JSON string
*/
void onRefresh(String featuresJson);

/**
* See {@link GBFeaturesRepository#onFeaturesRefresh(FeatureRefreshCallback)}
* @param throwable Exception on refreshCallback
*/
void onError(Throwable throwable);
}
20 changes: 15 additions & 5 deletions lib/src/main/java/growthbook/sdk/java/GBFeaturesRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,8 @@ public String getFeaturesJson() {

/**
* Subscribe to feature refresh events
* This callback is called when the features are successfully refreshed.
* This callback is called when the features are successfully refreshed or there is an error when refreshing.
* This is called even if the features have not changed.
* This will not be called if fetching the features results in a failure.
* @param callback This callback will be called when features are refreshed
*/
@Override
Expand All @@ -167,6 +166,7 @@ private void enqueueFeatureRefreshRequest() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
// OkHttp will auto-retry on failure
self.onRefreshFailed(e);
}

@Override
Expand Down Expand Up @@ -350,9 +350,7 @@ private void onResponseJson(String responseJsonString) throws FeatureFetchExcept

this.featuresJson = refreshedFeatures;

this.refreshCallbacks.forEach(featureRefreshCallback -> {
featureRefreshCallback.onRefresh(this.featuresJson);
});
this.onRefreshSuccess(this.featuresJson);
} catch (DecryptionUtils.DecryptionException e) {
e.printStackTrace();

Expand All @@ -363,6 +361,18 @@ private void onResponseJson(String responseJsonString) throws FeatureFetchExcept
}
}

private void onRefreshSuccess(String featuresJson) {
this.refreshCallbacks.forEach(featureRefreshCallback -> {
featureRefreshCallback.onRefresh(featuresJson);
});
}

private void onRefreshFailed(Throwable throwable) {
this.refreshCallbacks.forEach(featureRefreshCallback -> {
featureRefreshCallback.onError(throwable);
});
}

/**
* Handles the successful features fetching response
* @param response Successful response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class GBFeaturesRepositoryTest {
Expand Down Expand Up @@ -148,7 +153,82 @@ void canFetchEncryptedFeatures_mockedResponse() throws IOException, FeatureFetch
assertEquals(expected, subject.getFeaturesJson().trim());
}

@Test
void testOnFeaturesRefresh_Success() {
String fakeResponseJson = "{\"status\":200,\"features\":{\"banner_text\":{\"defaultValue\":\"Welcome to Acme Donuts!\",\"rules\":[{\"condition\":{\"country\":\"france\"},\"force\":\"Bienvenue au Beignets Acme !\"},{\"condition\":{\"country\":\"spain\"},\"force\":\"¡Bienvenidos y bienvenidas a Donas Acme!\"}]},\"dark_mode\":{\"defaultValue\":false,\"rules\":[{\"condition\":{\"loggedIn\":true},\"force\":true,\"coverage\":0.5,\"hashAttribute\":\"id\"}]},\"donut_price\":{\"defaultValue\":2.5,\"rules\":[{\"condition\":{\"employee\":true},\"force\":0}]},\"meal_overrides_gluten_free\":{\"defaultValue\":{\"meal_type\":\"standard\",\"dessert\":\"Strawberry Cheesecake\"},\"rules\":[{\"condition\":{\"dietaryRestrictions\":{\"$elemMatch\":{\"$eq\":\"gluten_free\"}}},\"force\":{\"meal_type\":\"gf\",\"dessert\":\"French Vanilla Ice Cream\"}}]}},\"dateUpdated\":\"2023-01-11T00:26:01.745Z\"}";

OkHttpClient mockOkHttpClient = mock(OkHttpClient.class);

Call mockCall = mock(Call.class);
doReturn(mockCall).when(mockOkHttpClient).newCall(any(Request.class));

Response response = new Response.Builder()
.request(new Request.Builder().url("http://url.com").build())
.protocol(Protocol.HTTP_1_1)
.code(200).message("").body(
ResponseBody.create(
fakeResponseJson,
MediaType.parse("application/json")
))
.build();

doAnswer(invocation -> {
Callback mockCallback = invocation.getArgument(0);
mockCallback.onResponse(mockCall, response);
return null;
}).when(mockCall).enqueue(any(Callback.class));

FeatureRefreshCallback featureRefreshCallback = mock(FeatureRefreshCallback.class);

GBFeaturesRepository subject = new GBFeaturesRepository(
"http://localhost:80",
"sdk-abc123",
null,
null,
0,
mockOkHttpClient
);

subject.onFeaturesRefresh(featureRefreshCallback);

subject.getFeaturesJson();

String expected = "{\"banner_text\":{\"defaultValue\":\"Welcome to Acme Donuts!\",\"rules\":[{\"condition\":{\"country\":\"france\"},\"force\":\"Bienvenue au Beignets Acme !\"},{\"condition\":{\"country\":\"spain\"},\"force\":\"¡Bienvenidos y bienvenidas a Donas Acme!\"}]},\"dark_mode\":{\"defaultValue\":false,\"rules\":[{\"condition\":{\"loggedIn\":true},\"force\":true,\"coverage\":0.5,\"hashAttribute\":\"id\"}]},\"donut_price\":{\"defaultValue\":2.5,\"rules\":[{\"condition\":{\"employee\":true},\"force\":0}]},\"meal_overrides_gluten_free\":{\"defaultValue\":{\"meal_type\":\"standard\",\"dessert\":\"Strawberry Cheesecake\"},\"rules\":[{\"condition\":{\"dietaryRestrictions\":{\"$elemMatch\":{\"$eq\":\"gluten_free\"}}},\"force\":{\"meal_type\":\"gf\",\"dessert\":\"French Vanilla Ice Cream\"}}]}}";
verify(featureRefreshCallback).onRefresh(expected);
verify(featureRefreshCallback, never()).onError(any(Throwable.class));
}

@Test
void testOnFeaturesRefresh_Error() {
OkHttpClient mockOkHttpClient = mock(OkHttpClient.class);
IOException requestFailed = new IOException("Request failed");

Call mockCall = mock(Call.class);
doReturn(mockCall).when(mockOkHttpClient).newCall(any(Request.class));
doAnswer(invocation -> {
Callback mockCallback = invocation.getArgument(0);
mockCallback.onFailure(mockCall, requestFailed);
return null;
}).when(mockCall).enqueue(any(Callback.class));

FeatureRefreshCallback featureRefreshCallback = mock(FeatureRefreshCallback.class);

GBFeaturesRepository subject = new GBFeaturesRepository(
"http://localhost:80",
"sdk-abc123",
null,
null,
0,
mockOkHttpClient
);

subject.onFeaturesRefresh(featureRefreshCallback);

subject.getFeaturesJson();

verify(featureRefreshCallback).onError(requestFailed);
verify(featureRefreshCallback, never()).onRefresh(anyString());
}

/*
@Test
Expand Down

0 comments on commit eaaa583

Please sign in to comment.