Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 1d06f77

Browse files
committed
Merge pull request #257 from cloudant/256-cce-403-rsn
Fixed 403 null reason deserialization
2 parents 13f628d + 36e2185 commit 1d06f77

File tree

4 files changed

+107
-60
lines changed

4 files changed

+107
-60
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [FIX] Documentation that suggested calling `database("dbname", false)` would immediately throw a
1717
`NoDocumentException` if the database did not exist. The exception is not thrown until the first
1818
operation on the `Database` instance.
19+
- [FIX] `ClassCastException` when the server responded `403` with a `null` reason in the JSON.
1920

2021
# 2.4.3 (2016-05-05)
2122
- [IMPROVED] Reduced the length of the User-Agent header string.

cloudant-client/src/main/java/com/cloudant/client/org/lightcouch/CouchDbClient.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -475,10 +475,8 @@ public HttpConnection execute(HttpConnection connection) {
475475
try {
476476
JsonObject errorResponse = new Gson().fromJson(e.error, JsonObject
477477
.class);
478-
exception.error = errorResponse.getAsJsonPrimitive
479-
("error").getAsString();
480-
exception.reason = errorResponse.getAsJsonPrimitive
481-
("reason").getAsString();
478+
exception.error = getAsString(errorResponse, "error");
479+
exception.reason = getAsString(errorResponse, "reason");
482480
} catch (JsonParseException jpe) {
483481
exception.error = e.error;
484482
}

cloudant-client/src/main/java/com/cloudant/client/org/lightcouch/internal/CouchDbUtil.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,17 @@ public static <T> T jsonToObject(Gson gson, JsonElement elem, String key, Class<
9393
}
9494

9595
/**
96-
* @return A JSON element as a String, or null if not found.
96+
* @return A JSON element as a String, or null if there is no member with that name or the
97+
* value was a JSON null.
9798
*/
9899
public static String getAsString(JsonObject j, String e) {
99-
return (j.get(e) == null) ? null : j.get(e).getAsString();
100+
if (j != null && e != null) {
101+
JsonElement element = j.get(e);
102+
if (element != null && !element.isJsonNull()) {
103+
return element.getAsString();
104+
}
105+
}
106+
return null;
100107
}
101108

102109
/**

cloudant-client/src/test/java/com/cloudant/tests/HttpTest.java

Lines changed: 95 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
import com.cloudant.tests.util.MockWebServerResources;
2525
import com.cloudant.tests.util.Utils;
2626
import com.google.gson.Gson;
27+
import com.google.gson.JsonElement;
28+
import com.google.gson.JsonNull;
2729
import com.google.gson.JsonObject;
30+
import com.google.gson.JsonPrimitive;
2831
import com.squareup.okhttp.mockwebserver.Dispatcher;
2932
import com.squareup.okhttp.mockwebserver.MockResponse;
3033
import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -228,43 +231,6 @@ public void cookieInterceptorURLEncoding() throws Exception {
228231
}
229232
}
230233

231-
/**
232-
* This test checks that the cookie is successfully renewed if a 403 with an error of
233-
* "credentials_expired" is returned.
234-
*
235-
* @throws Exception
236-
*/
237-
@Test
238-
public void cookie403Renewal() throws Exception {
239-
240-
// Request sequence
241-
// _session request to get Cookie
242-
// GET request -> 403
243-
// _session for new cookie
244-
// GET replay -> 200
245-
mockWebServer.enqueue(MockWebServerResources.OK_COOKIE);
246-
mockWebServer.enqueue(new MockResponse().setResponseCode(403).setBody
247-
("{\"error\":\"credentials_expired\", \"reason\":\"Session expired\"}\r\n"));
248-
mockWebServer.enqueue(MockWebServerResources.OK_COOKIE);
249-
mockWebServer.enqueue(new MockResponse());
250-
251-
CloudantClient c = CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer)
252-
.username("a")
253-
.password("b")
254-
.build();
255-
//the GET request will try to get a session, then perform the GET
256-
//the GET will result in a 403, which should mean another request to _session
257-
//followed by a replay of GET
258-
c.executeRequest(Http.GET(c.getBaseUri()));
259-
260-
//if we don't handle the 403 correctly an exception will be thrown
261-
262-
// also assert that there were 4 calls
263-
assertEquals("The server should have received 4 requests", 4, mockWebServer
264-
.getRequestCount());
265-
266-
}
267-
268234
/**
269235
* This test check that the cookie is renewed if the server presents a Set-Cookie header
270236
* after the cookie authentication.
@@ -278,7 +244,7 @@ public void cookieRenewal() throws Exception {
278244
"AuthSession=\"RenewCookie_a2ltc3RlYmVsOjUxMzRBQTUzOtiY2_IDUIdsTJEVNEjObAbyhrgz\";";
279245
// Request sequence
280246
// _session request to get Cookie
281-
// GET request -> 403
247+
// GET request -> 200 with a Set-Cookie
282248
// _session for new cookie
283249
// GET replay -> 200
284250
mockWebServer.enqueue(MockWebServerResources.OK_COOKIE);
@@ -312,6 +278,19 @@ public void cookieRenewal() throws Exception {
312278
"\"", headerValue);
313279
}
314280

281+
/**
282+
* This test checks that the cookie is successfully renewed if a 403 with an error of
283+
* "credentials_expired" is returned.
284+
*
285+
* @throws Exception
286+
*/
287+
@Test
288+
public void cookie403Renewal() throws Exception {
289+
290+
// Test for a 403 with expired credentials, should result in 4 requests
291+
basic403Test("credentials_expired", "Session expired", 4);
292+
}
293+
315294
/**
316295
* This test checks that if we get a 403 that is not an error of "credentials_expired" then
317296
* the exception is correctly thrown and the error stream is deserialized. This is important
@@ -322,28 +301,90 @@ public void cookieRenewal() throws Exception {
322301
@Test
323302
public void handleNonExpiry403() throws Exception {
324303

325-
// Request sequence
326-
// _session request to get Cookie
327-
// GET request -> 403 (CouchDbException)
304+
// Test for a non-expiry 403, expect 2 requests
305+
basic403Test("403_not_expired_test", "example reason", 2);
306+
}
307+
308+
/**
309+
* Same as {@link #handleNonExpiry403()} but with no reason property in the JSON.
310+
*
311+
* @throws Exception
312+
*/
313+
@Test
314+
public void handleNonExpiry403NoReason() throws Exception {
315+
316+
// Test for a non-expiry 403, expect 2 requests
317+
basic403Test("403_not_expired_test", null, 2);
318+
}
319+
320+
/**
321+
* * Same as {@link #handleNonExpiry403()} but with a {@code null} reason property in the JSON.
322+
*
323+
* @throws Exception
324+
*/
325+
@Test
326+
public void handleNonExpiry403NullReason() throws Exception {
327+
328+
// Test for a non-expiry 403, expect 2 requests
329+
basic403Test("403_not_expired_test", "null", 2);
330+
}
331+
332+
/**
333+
* Method that performs a basic test for a 403 response. The sequence of requests is:
334+
* <OL>
335+
* <LI>_session request to get Cookie</LI>
336+
* <LI>GET request -> a 403 response</LI>
337+
* <LI>_session for new cookie*</LI>
338+
* <LI>GET replay -> a 200 response*</LI>
339+
* </OL>
340+
* The requests annotated * should only happen in the credentials_expired 403 case
341+
*
342+
* @param error the response JSON error content for the 403
343+
* @param reason the response JSON reason content for the 403
344+
*/
345+
private void basic403Test(String error, String reason, int expectedRequests) throws
346+
Exception {
328347
mockWebServer.enqueue(MockWebServerResources.OK_COOKIE);
329-
mockWebServer.enqueue(new MockResponse().setResponseCode(403).setBody
330-
("{\"error\":\"403_not_expired_test\", \"reason\":\"example reason\"}\r\n"));
348+
JsonObject responseBody = new JsonObject();
349+
responseBody.add("error", new JsonPrimitive(error));
350+
JsonElement jsonReason;
351+
if (reason != null) {
352+
if ("null".equals(reason)) {
353+
jsonReason = JsonNull.INSTANCE;
354+
reason = null; // For the assertion we need a real null, not a JsonNull
355+
} else {
356+
jsonReason = new JsonPrimitive(reason);
357+
}
358+
responseBody.add("reason", jsonReason);
359+
}
360+
mockWebServer.enqueue(new MockResponse().setResponseCode(403).setBody(responseBody
361+
.toString()));
362+
mockWebServer.enqueue(MockWebServerResources.OK_COOKIE);
363+
mockWebServer.enqueue(new MockResponse());
364+
365+
CloudantClient c = CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer)
366+
.username("a")
367+
.password("b")
368+
.build();
331369

370+
//the GET request will try to get a session, then perform the GET
371+
//the GET will result in a 403, which in a renewal case should mean another request to
372+
// _session followed by a replay of GET
332373
try {
333-
CloudantClient c = CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer)
334-
.username("a")
335-
.password("b")
336-
.build();
337-
//the GET request will try to get a session, then perform the GET
338-
//the GET will result in a 403, which should result in a CouchDbException
339374
c.executeRequest(Http.GET(c.getBaseUri()));
340-
fail("A 403 not due to cookie expiry should result in a CouchDbException");
375+
if (!error.equals("credentials_expired")) {
376+
fail("A 403 not due to cookie expiry should result in a CouchDbException");
377+
}
341378
} catch (CouchDbException e) {
342-
e.printStackTrace();
343-
assertNotNull("The error should not be null", e.getError());
344-
assertEquals("The error message should be the expected one", "403_not_expired_test", e
345-
.getError());
379+
assertEquals("The exception error should be the expected message", error, e.getError());
380+
assertEquals("The exception reason should be the expected message", reason, e
381+
.getReason());
346382
}
383+
384+
// also assert that there were the correct number of calls
385+
assertEquals("The server should receive the expected number of requests",
386+
expectedRequests, mockWebServer
387+
.getRequestCount());
347388
}
348389

349390
@Test

0 commit comments

Comments
 (0)