Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import org.json.JSONObject
* Sealed result returned by every Rethink server billing endpoint.
*
* Use [RpnPurchaseAckServerResponse.from] to deserialise raw HTTP response bodies.
* A 2xx response with `"success" != false` is treated as [Ok]; everything
* else (non-2xx, `"error"` key present, or `"success" == false`) is [Err].
* A 2xx response with a JSON body and `"success" != false` is treated as [Ok];
* everything else (non-2xx, missing or non-JSON body, `"error"` key present, or
* `"success" == false`) is [Err].
*/
sealed class RpnPurchaseAckServerResponse {
data class Ok(val payload: ResponseOk) : RpnPurchaseAckServerResponse()
Expand All @@ -48,7 +49,14 @@ sealed class RpnPurchaseAckServerResponse {
return Err(payload)
}

val json = body?.trimmedJson() ?: JSONObject()
// The server always replies with a JSON object (it carries at least "ray"),
// so a missing or non-JSON 2xx body (e.g. from a proxy) is not a success.
val json = body?.trimmedJson()
if (json == null) {
val payload = ResponseErr(error = "malformed response", httpCode = httpCode)
Logger.d(LOG_IAB, "PlayErr: $payload")
return Err(payload)
}

// If "error" key is present, or "success" is explicitly false → error
return if (json.has("error") || !json.optBoolean("success", true)) {
Expand All @@ -63,10 +71,10 @@ sealed class RpnPurchaseAckServerResponse {
}
}

private fun String.trimmedJson(): JSONObject = try {
private fun String.trimmedJson(): JSONObject? = try {
JSONObject(this.trim())
} catch (_: Exception) {
JSONObject()
null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,20 @@ class RpnPurchaseAckServerResponseTest {
assertTrue(result is RpnPurchaseAckServerResponse.Ok)
assertFalse((result as RpnPurchaseAckServerResponse.Ok).payload.isCancelled)
}

@Test
fun `2xx without a body is not ok`() {
val result = RpnPurchaseAckServerResponse.from(null, 200)

assertTrue(result is RpnPurchaseAckServerResponse.Err)
assertEquals(200, (result as RpnPurchaseAckServerResponse.Err).payload.httpCode)
}

@Test
fun `non-json 2xx body from a proxy is not ok`() {
val result = RpnPurchaseAckServerResponse.from("<html>Login required</html>", 200)

assertTrue(result is RpnPurchaseAckServerResponse.Err)
assertFalse((result as RpnPurchaseAckServerResponse.Err).payload.isPurchaseCancelled)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,20 @@ class RpnPurchaseAckServerResponseTest {
assertTrue(result is RpnPurchaseAckServerResponse.Ok)
assertFalse((result as RpnPurchaseAckServerResponse.Ok).payload.isCancelled)
}

@Test
fun `2xx without a body is not ok`() {
val result = RpnPurchaseAckServerResponse.from(null, 200)

assertTrue(result is RpnPurchaseAckServerResponse.Err)
assertEquals(200, (result as RpnPurchaseAckServerResponse.Err).payload.httpCode)
}

@Test
fun `non-json 2xx body from a proxy is not ok`() {
val result = RpnPurchaseAckServerResponse.from("<html>Login required</html>", 200)

assertTrue(result is RpnPurchaseAckServerResponse.Err)
assertFalse((result as RpnPurchaseAckServerResponse.Err).payload.isPurchaseCancelled)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import org.json.JSONObject
* Sealed result returned by every Rethink server billing endpoint.
*
* Use [RpnPurchaseAckServerResponse.from] to deserialise raw HTTP response bodies.
* A 2xx response with `"success" != false` is treated as [Ok]; everything
* else (non-2xx, `"error"` key present, or `"success" == false`) is [Err].
* A 2xx response with a JSON body and `"success" != false` is treated as [Ok];
* everything else (non-2xx, missing or non-JSON body, `"error"` key present, or
* `"success" == false`) is [Err].
*/
sealed class RpnPurchaseAckServerResponse {
data class Ok(val payload: ResponseOk) : RpnPurchaseAckServerResponse()
Expand All @@ -48,7 +49,14 @@ sealed class RpnPurchaseAckServerResponse {
return Err(payload)
}

val json = body?.trimmedJson() ?: JSONObject()
// The server always replies with a JSON object (it carries at least "ray"),
// so a missing or non-JSON 2xx body (e.g. from a proxy) is not a success.
val json = body?.trimmedJson()
if (json == null) {
val payload = ResponseErr(error = "malformed response", httpCode = httpCode)
Logger.d(LOG_IAB, "PlayErr: $payload")
return Err(payload)
}

// If "error" key is present, or "success" is explicitly false → error
return if (json.has("error") || !json.optBoolean("success", true)) {
Expand All @@ -63,10 +71,10 @@ sealed class RpnPurchaseAckServerResponse {
}
}

private fun String.trimmedJson(): JSONObject = try {
private fun String.trimmedJson(): JSONObject? = try {
JSONObject(this.trim())
} catch (_: Exception) {
JSONObject()
null
}
}
}
Expand Down