Skip to content

Commit a681c49

Browse files
committed
Handle the case where a tester has access to 0 releases
1 parent 1986969 commit a681c49

File tree

4 files changed

+120
-88
lines changed

4 files changed

+120
-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: 95 additions & 84 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,59 +106,60 @@ class FirebaseAppDistributionTesterApiClient {
88106
? BinaryType.APK
89107
: BinaryType.AAB;
90108

91-
newRelease =
92-
AppDistributionReleaseInternal.builder()
93-
.setDisplayVersion(displayVersion)
94-
.setBuildVersion(buildVersion)
95-
.setReleaseNotes(releaseNotes)
96-
.setBinaryType(binaryType)
97-
.setIasArtifactId(iasArtifactId)
98-
.setCodeHash(codeHash)
99-
.setApkHash(apkHash)
100-
.setDownloadUrl(downloadUrl)
101-
.build();
102-
inputStream.close();
103-
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();
109+
AppDistributionReleaseInternal newRelease = AppDistributionReleaseInternal.builder()
110+
.setDisplayVersion(displayVersion)
111+
.setBuildVersion(buildVersion)
112+
.setReleaseNotes(releaseNotes)
113+
.setBinaryType(binaryType)
114+
.setIasArtifactId(iasArtifactId)
115+
.setCodeHash(codeHash)
116+
.setApkHash(apkHash)
117+
.setDownloadUrl(downloadUrl)
118+
.build();
119+
120+
LogWrapper.getInstance().v("Zip hash for the new release " + newRelease.getApkHash());
121+
return newRelease;
122+
} catch (JSONException e) {
123+
LogWrapper.getInstance().e(TAG + "Error parsing the new release.", e);
124+
throw new FirebaseAppDistributionException(
125+
ErrorMessages.JSON_PARSING_ERROR, Status.UNKNOWN, e);
113126
}
114-
LogWrapper.getInstance().v("Zip hash for the new release " + newRelease.getApkHash());
115-
return newRelease;
116127
}
117128

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

@@ -152,33 +171,27 @@ private String tryGetValue(JSONObject jsonObject, String key) {
152171
}
153172
}
154173

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)
174+
HttpsURLConnection openHttpsUrlConnection(String appId, String fid, String apiKey,
175+
String authToken, Context context)
173176
throws FirebaseAppDistributionException {
174177
HttpsURLConnection httpsURLConnection;
175178
URL url = getReleasesEndpointUrl(appId, fid);
176179
try {
177180
httpsURLConnection = (HttpsURLConnection) url.openConnection();
178181
} catch (IOException e) {
179182
throw new FirebaseAppDistributionException(
180-
Constants.ErrorMessages.NETWORK_ERROR, NETWORK_FAILURE, e);
183+
ErrorMessages.NETWORK_ERROR, Status.NETWORK_FAILURE, e);
184+
}
185+
try {
186+
httpsURLConnection.setRequestMethod(REQUEST_METHOD);
187+
} catch (ProtocolException e) {
188+
throw new FirebaseAppDistributionException(ErrorMessages.UNKNOWN_ERROR, Status.UNKNOWN, e);
181189
}
190+
httpsURLConnection.setRequestProperty(API_KEY_HEADER, apiKey);
191+
httpsURLConnection.setRequestProperty(INSTALLATION_AUTH_HEADER, authToken);
192+
httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName());
193+
httpsURLConnection.addRequestProperty(
194+
X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage(context));
182195
return httpsURLConnection;
183196
}
184197

@@ -188,7 +201,7 @@ private URL getReleasesEndpointUrl(String appId, String fid)
188201
return new URL(String.format(RELEASE_ENDPOINT_URL_FORMAT, appId, fid));
189202
} catch (MalformedURLException e) {
190203
throw new FirebaseAppDistributionException(
191-
Constants.ErrorMessages.UNKNOWN_ERROR,
204+
ErrorMessages.UNKNOWN_ERROR,
192205
FirebaseAppDistributionException.Status.UNKNOWN,
193206
e);
194207
}
@@ -206,8 +219,6 @@ private static String convertInputStreamToString(InputStream is) throws IOExcept
206219

207220
/**
208221
* Gets the Android package's SHA-1 fingerprint.
209-
*
210-
* @param context
211222
*/
212223
private String getFingerprintHashForPackage(Context context) {
213224
byte[] hash;
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: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
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

@@ -47,7 +48,8 @@ public class FirebaseAppDistributionTesterApiClientTest {
4748

4849
private FirebaseAppDistributionTesterApiClient firebaseAppDistributionTesterApiClient;
4950
private Context applicationContext;
50-
@Mock private HttpsURLConnection mockHttpsURLConnection;
51+
@Mock
52+
private HttpsURLConnection mockHttpsURLConnection;
5153

5254
@Before
5355
public void setup() throws Exception {
@@ -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
@@ -197,6 +200,18 @@ public void fetchNewRelease_whenInvalidJson_throwsError() throws Exception {
197200
assert (ex.getCause() instanceof JSONException);
198201
}
199202

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

0 commit comments

Comments
 (0)