Skip to content

Commit c971713

Browse files
authored
Handle the case where a tester has access to 0 releases (#3229)
* Handle the case where a tester has access to 0 releases * Apply Google formatting
1 parent 28e1a16 commit c971713

File tree

4 files changed

+118
-88
lines changed

4 files changed

+118
-88
lines changed

firebase-app-distribution/src/main/java/com/google/firebase/app/distribution/CheckForNewReleaseClient.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ AppDistributionReleaseInternal getNewReleaseFromClient(
119119
firebaseAppDistributionTesterApiClient.fetchNewRelease(
120120
fid, appId, apiKey, authToken, firebaseApp.getApplicationContext());
121121

122+
if (retrievedNewRelease == null) {
123+
LogWrapper.getInstance().v(TAG + "Tester does not have access to any releases");
124+
return null;
125+
}
126+
122127
if (!canInstall(retrievedNewRelease)) {
123128
LogWrapper.getInstance().v(TAG + "New release has lower version code than current release");
124129
return null;

firebase-app-distribution/src/main/java/com/google/firebase/app/distribution/FirebaseAppDistributionTesterApiClient.java

Lines changed: 86 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,23 @@
1414

1515
package com.google.firebase.app.distribution;
1616

17-
import static com.google.firebase.app.distribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;
18-
import static com.google.firebase.app.distribution.FirebaseAppDistributionException.Status.NETWORK_FAILURE;
19-
2017
import android.content.Context;
2118
import android.content.pm.PackageManager;
2219
import androidx.annotation.NonNull;
20+
import androidx.annotation.Nullable;
2321
import com.google.android.gms.common.util.AndroidUtilsLight;
2422
import com.google.android.gms.common.util.Hex;
23+
import com.google.firebase.app.distribution.Constants.ErrorMessages;
24+
import com.google.firebase.app.distribution.FirebaseAppDistributionException.Status;
2525
import java.io.BufferedInputStream;
2626
import java.io.ByteArrayOutputStream;
2727
import java.io.IOException;
2828
import java.io.InputStream;
2929
import java.net.MalformedURLException;
30+
import java.net.ProtocolException;
3031
import java.net.URL;
3132
import javax.net.ssl.HttpsURLConnection;
33+
import org.json.JSONArray;
3234
import org.json.JSONException;
3335
import org.json.JSONObject;
3436

@@ -55,26 +57,42 @@ class FirebaseAppDistributionTesterApiClient {
5557

5658
public static final int DEFAULT_BUFFER_SIZE = 8192;
5759

58-
public @NonNull AppDistributionReleaseInternal fetchNewRelease(
60+
/**
61+
* Fetches and returns the lastest release for the app that the tester has access to, or null if
62+
* the tester doesn't have access to any releases.
63+
*/
64+
@Nullable
65+
public AppDistributionReleaseInternal fetchNewRelease(
5966
@NonNull String fid,
6067
@NonNull String appId,
6168
@NonNull String apiKey,
6269
@NonNull String authToken,
6370
@NonNull Context context)
6471
throws FirebaseAppDistributionException {
72+
HttpsURLConnection connection = openHttpsUrlConnection(appId, fid, apiKey, authToken, context);
73+
String responseBody;
74+
try (BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
75+
responseBody = convertInputStreamToString(inputStream);
76+
} catch (IOException e) {
77+
throw getExceptionForHttpResponse(connection, e);
78+
} finally {
79+
connection.disconnect();
80+
}
81+
return parseNewRelease(responseBody);
82+
}
6583

66-
AppDistributionReleaseInternal newRelease;
67-
HttpsURLConnection connection = openHttpsUrlConnection(appId, fid);
84+
AppDistributionReleaseInternal parseNewRelease(String responseBody)
85+
throws FirebaseAppDistributionException {
6886
try {
69-
connection.setRequestMethod(REQUEST_METHOD);
70-
connection.setRequestProperty(API_KEY_HEADER, apiKey);
71-
connection.setRequestProperty(INSTALLATION_AUTH_HEADER, authToken);
72-
connection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName());
73-
connection.addRequestProperty(
74-
X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage(context));
75-
76-
InputStream inputStream = connection.getInputStream();
77-
JSONObject newReleaseJson = readFetchReleaseInputStream(inputStream);
87+
JSONObject responseJson = new JSONObject(responseBody);
88+
if (!responseJson.has("releases")) {
89+
return null;
90+
}
91+
JSONArray releasesJson = responseJson.getJSONArray("releases");
92+
if (releasesJson.length() == 0) {
93+
return null;
94+
}
95+
JSONObject newReleaseJson = releasesJson.getJSONObject(0);
7896
final String displayVersion = newReleaseJson.getString(DISPLAY_VERSION_JSON_KEY);
7997
final String buildVersion = newReleaseJson.getString(BUILD_VERSION_JSON_KEY);
8098
String releaseNotes = tryGetValue(newReleaseJson, RELEASE_NOTES_JSON_KEY);
@@ -88,7 +106,7 @@ class FirebaseAppDistributionTesterApiClient {
88106
? BinaryType.APK
89107
: BinaryType.AAB;
90108

91-
newRelease =
109+
AppDistributionReleaseInternal newRelease =
92110
AppDistributionReleaseInternal.builder()
93111
.setDisplayVersion(displayVersion)
94112
.setBuildVersion(buildVersion)
@@ -99,48 +117,50 @@ class FirebaseAppDistributionTesterApiClient {
99117
.setApkHash(apkHash)
100118
.setDownloadUrl(downloadUrl)
101119
.build();
102-
inputStream.close();
103120

104-
} catch (IOException | JSONException e) {
105-
if (e instanceof JSONException) {
106-
LogWrapper.getInstance().e(TAG + "Error parsing the new release.", e);
107-
throw new FirebaseAppDistributionException(
108-
Constants.ErrorMessages.JSON_PARSING_ERROR, NETWORK_FAILURE, e);
109-
}
110-
throw getExceptionForHttpResponse(connection);
111-
} finally {
112-
connection.disconnect();
121+
LogWrapper.getInstance().v("Zip hash for the new release " + newRelease.getApkHash());
122+
return newRelease;
123+
} catch (JSONException e) {
124+
LogWrapper.getInstance().e(TAG + "Error parsing the new release.", e);
125+
throw new FirebaseAppDistributionException(
126+
ErrorMessages.JSON_PARSING_ERROR, Status.UNKNOWN, e);
113127
}
114-
LogWrapper.getInstance().v("Zip hash for the new release " + newRelease.getApkHash());
115-
return newRelease;
116128
}
117129

118130
private FirebaseAppDistributionException getExceptionForHttpResponse(
119-
HttpsURLConnection connection) {
131+
HttpsURLConnection connection, Exception cause) {
132+
// TODO(lkellogg): this try-catch should be unnecessary because it will only throw an
133+
// IOException here if we couldn't connect to the server, in which case getInputStream() would
134+
// have already failed with the same exception. We also weirdly have to choose one of the two
135+
// thrown exceptions to set as the cause. We can avoid this by checking the response code
136+
// first, and then catching any unexpected exceptions when reading the input stream, essentially
137+
// combining the "default" case below with this try-catch.
138+
int responseCode;
120139
try {
121-
LogWrapper.getInstance().e(TAG + "Failed due to " + connection.getResponseCode());
122-
switch (connection.getResponseCode()) {
123-
case 401:
124-
return new FirebaseAppDistributionException(
125-
Constants.ErrorMessages.AUTHENTICATION_ERROR, AUTHENTICATION_FAILURE);
126-
case 403:
127-
case 400:
128-
return new FirebaseAppDistributionException(
129-
Constants.ErrorMessages.AUTHORIZATION_ERROR, AUTHENTICATION_FAILURE);
130-
case 404:
131-
return new FirebaseAppDistributionException(
132-
Constants.ErrorMessages.NOT_FOUND_ERROR, AUTHENTICATION_FAILURE);
133-
case 408:
134-
case 504:
135-
return new FirebaseAppDistributionException(
136-
Constants.ErrorMessages.TIMEOUT_ERROR, NETWORK_FAILURE);
137-
default:
138-
return new FirebaseAppDistributionException(
139-
Constants.ErrorMessages.NETWORK_ERROR, NETWORK_FAILURE);
140-
}
141-
} catch (IOException ex) {
140+
responseCode = connection.getResponseCode();
141+
} catch (IOException e) {
142142
return new FirebaseAppDistributionException(
143-
Constants.ErrorMessages.NETWORK_ERROR, NETWORK_FAILURE, ex);
143+
ErrorMessages.NETWORK_ERROR, Status.NETWORK_FAILURE, e);
144+
}
145+
LogWrapper.getInstance().e(TAG + "Failed due to " + responseCode);
146+
switch (responseCode) {
147+
case 401:
148+
return new FirebaseAppDistributionException(
149+
ErrorMessages.AUTHENTICATION_ERROR, Status.AUTHENTICATION_FAILURE, cause);
150+
case 403:
151+
case 400:
152+
return new FirebaseAppDistributionException(
153+
ErrorMessages.AUTHORIZATION_ERROR, Status.AUTHENTICATION_FAILURE, cause);
154+
case 404:
155+
return new FirebaseAppDistributionException(
156+
ErrorMessages.NOT_FOUND_ERROR, Status.AUTHENTICATION_FAILURE, cause);
157+
case 408:
158+
case 504:
159+
return new FirebaseAppDistributionException(
160+
ErrorMessages.TIMEOUT_ERROR, Status.NETWORK_FAILURE, cause);
161+
default:
162+
return new FirebaseAppDistributionException(
163+
ErrorMessages.UNKNOWN_ERROR, Status.UNKNOWN, cause);
144164
}
145165
}
146166

@@ -152,33 +172,27 @@ private String tryGetValue(JSONObject jsonObject, String key) {
152172
}
153173
}
154174

155-
private JSONObject readFetchReleaseInputStream(InputStream in)
156-
throws FirebaseAppDistributionException, IOException {
157-
JSONObject newRelease;
158-
InputStream jsonIn = new BufferedInputStream(in);
159-
String result = convertInputStreamToString(jsonIn);
160-
try {
161-
JSONObject json = new JSONObject(result);
162-
newRelease = json.getJSONArray("releases").getJSONObject(0);
163-
} catch (JSONException e) {
164-
throw new FirebaseAppDistributionException(
165-
Constants.ErrorMessages.JSON_PARSING_ERROR,
166-
FirebaseAppDistributionException.Status.UNKNOWN,
167-
e);
168-
}
169-
return newRelease;
170-
}
171-
172-
HttpsURLConnection openHttpsUrlConnection(String appId, String fid)
175+
HttpsURLConnection openHttpsUrlConnection(
176+
String appId, String fid, String apiKey, String authToken, Context context)
173177
throws FirebaseAppDistributionException {
174178
HttpsURLConnection httpsURLConnection;
175179
URL url = getReleasesEndpointUrl(appId, fid);
176180
try {
177181
httpsURLConnection = (HttpsURLConnection) url.openConnection();
178182
} catch (IOException e) {
179183
throw new FirebaseAppDistributionException(
180-
Constants.ErrorMessages.NETWORK_ERROR, NETWORK_FAILURE, e);
184+
ErrorMessages.NETWORK_ERROR, Status.NETWORK_FAILURE, e);
181185
}
186+
try {
187+
httpsURLConnection.setRequestMethod(REQUEST_METHOD);
188+
} catch (ProtocolException e) {
189+
throw new FirebaseAppDistributionException(ErrorMessages.UNKNOWN_ERROR, Status.UNKNOWN, e);
190+
}
191+
httpsURLConnection.setRequestProperty(API_KEY_HEADER, apiKey);
192+
httpsURLConnection.setRequestProperty(INSTALLATION_AUTH_HEADER, authToken);
193+
httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName());
194+
httpsURLConnection.addRequestProperty(
195+
X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage(context));
182196
return httpsURLConnection;
183197
}
184198

@@ -188,9 +202,7 @@ private URL getReleasesEndpointUrl(String appId, String fid)
188202
return new URL(String.format(RELEASE_ENDPOINT_URL_FORMAT, appId, fid));
189203
} catch (MalformedURLException e) {
190204
throw new FirebaseAppDistributionException(
191-
Constants.ErrorMessages.UNKNOWN_ERROR,
192-
FirebaseAppDistributionException.Status.UNKNOWN,
193-
e);
205+
ErrorMessages.UNKNOWN_ERROR, FirebaseAppDistributionException.Status.UNKNOWN, e);
194206
}
195207
}
196208

@@ -204,11 +216,7 @@ private static String convertInputStreamToString(InputStream is) throws IOExcept
204216
return result.toString();
205217
}
206218

207-
/**
208-
* Gets the Android package's SHA-1 fingerprint.
209-
*
210-
* @param context
211-
*/
219+
/** Gets the Android package's SHA-1 fingerprint. */
212220
private String getFingerprintHashForPackage(Context context) {
213221
byte[] hash;
214222

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

firebase-app-distribution/src/test/java/com/google/firebase/app/distribution/FirebaseAppDistributionTesterApiClientTest.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
import static androidx.test.InstrumentationRegistry.getContext;
1818
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.assertNull;
1920
import static org.junit.Assert.assertThrows;
2021
import static org.mockito.Mockito.when;
2122

2223
import android.content.Context;
2324
import androidx.test.core.app.ApplicationProvider;
25+
import com.google.firebase.app.distribution.FirebaseAppDistributionException.Status;
2426
import java.io.ByteArrayInputStream;
2527
import java.io.IOException;
2628
import java.io.InputStream;
@@ -58,11 +60,12 @@ public void setup() throws Exception {
5860
firebaseAppDistributionTesterApiClient =
5961
Mockito.spy(new FirebaseAppDistributionTesterApiClient());
6062

63+
applicationContext = ApplicationProvider.getApplicationContext();
64+
6165
Mockito.doReturn(mockHttpsURLConnection)
6266
.when(firebaseAppDistributionTesterApiClient)
63-
.openHttpsUrlConnection(TEST_APP_ID_1, TEST_FID_1);
64-
65-
applicationContext = ApplicationProvider.getApplicationContext();
67+
.openHttpsUrlConnection(
68+
TEST_APP_ID_1, TEST_FID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext);
6669
}
6770

6871
@Test
@@ -112,7 +115,7 @@ public void fetchNewRelease_whenResponseFailsWith401_throwsError() throws Except
112115
firebaseAppDistributionTesterApiClient.fetchNewRelease(
113116
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));
114117

115-
assertEquals(FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
118+
assertEquals(Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
116119
assertEquals("Failed to authenticate the tester", ex.getMessage());
117120
}
118121

@@ -128,7 +131,7 @@ public void fetchNewRelease_whenResponseFailsWith403_throwsError() throws Except
128131
firebaseAppDistributionTesterApiClient.fetchNewRelease(
129132
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));
130133

131-
assertEquals(FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
134+
assertEquals(Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
132135
assertEquals("Failed to authorize the tester", ex.getMessage());
133136
}
134137

@@ -144,7 +147,7 @@ public void fetchNewRelease_whenResponseFailsWith404_throwsError() throws Except
144147
firebaseAppDistributionTesterApiClient.fetchNewRelease(
145148
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));
146149

147-
assertEquals(FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
150+
assertEquals(Status.AUTHENTICATION_FAILURE, ex.getErrorCode());
148151
assertEquals("Tester or release not found", ex.getMessage());
149152
}
150153

@@ -160,7 +163,7 @@ public void fetchNewRelease_whenResponseFailsWith504_throwsError() throws Except
160163
firebaseAppDistributionTesterApiClient.fetchNewRelease(
161164
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));
162165

163-
assertEquals(FirebaseAppDistributionException.Status.NETWORK_FAILURE, ex.getErrorCode());
166+
assertEquals(Status.NETWORK_FAILURE, ex.getErrorCode());
164167
assertEquals("Failed to fetch releases due to timeout", ex.getMessage());
165168
}
166169

@@ -176,8 +179,9 @@ public void fetchNewRelease_whenResponseFailsWithUnknownCode_throwsError() throw
176179
firebaseAppDistributionTesterApiClient.fetchNewRelease(
177180
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));
178181

179-
assertEquals(FirebaseAppDistributionException.Status.NETWORK_FAILURE, ex.getErrorCode());
180-
assertEquals("Failed to fetch releases due to unknown network error", ex.getMessage());
182+
assertEquals(Status.UNKNOWN, ex.getErrorCode());
183+
assertEquals("Unknown Error", ex.getMessage());
184+
assertEquals(IOException.class, ex.getCause().getClass());
181185
}
182186

183187
@Test
@@ -192,11 +196,23 @@ public void fetchNewRelease_whenInvalidJson_throwsError() throws Exception {
192196
firebaseAppDistributionTesterApiClient.fetchNewRelease(
193197
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext));
194198

195-
assertEquals(FirebaseAppDistributionException.Status.UNKNOWN, ex.getErrorCode());
199+
assertEquals(Status.UNKNOWN, ex.getErrorCode());
196200
assertEquals("Error parsing service response", ex.getMessage());
197201
assert (ex.getCause() instanceof JSONException);
198202
}
199203

204+
@Test
205+
public void fetchNewRelease_whenNoReleases_returnsNull() throws Exception {
206+
JSONObject releaseJson = getTestJSON("testNoReleasesResponse.json");
207+
InputStream response =
208+
new ByteArrayInputStream(releaseJson.toString().getBytes(StandardCharsets.UTF_8));
209+
when(mockHttpsURLConnection.getInputStream()).thenReturn(response);
210+
AppDistributionReleaseInternal release =
211+
firebaseAppDistributionTesterApiClient.fetchNewRelease(
212+
TEST_FID_1, TEST_APP_ID_1, TEST_API_KEY, TEST_AUTH_TOKEN, applicationContext);
213+
assertNull(release);
214+
}
215+
200216
private JSONObject getTestJSON(String fileName) throws IOException, JSONException {
201217
final InputStream jsonInputStream = getContext().getResources().getAssets().open(fileName);
202218
final String testJsonString = streamToString(jsonInputStream);

0 commit comments

Comments
 (0)