From 17b4ff3527e9c2f40a307aa8ea0d0747ea4a5635 Mon Sep 17 00:00:00 2001 From: Konstantin Kostov Date: Fri, 25 Sep 2026 12:04:07 +0200 Subject: [PATCH] fix: in case of 401, capture any server reason as a UnauthorizedApiException --- .../eu/pretix/libpretixsync/api/PretixApi.kt | 16 ++- .../api/UnauthorizedApiException.java | 9 ++ .../api/PretixApiUnauthorizedTest.kt | 104 ++++++++++++++++++ 3 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 libpretixsync/src/main/java/eu/pretix/libpretixsync/api/UnauthorizedApiException.java create mode 100644 libpretixsync/src/test/java/eu/pretix/libpretixsync/api/PretixApiUnauthorizedTest.kt diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt index a7e30fec..fbe48785 100644 --- a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/PretixApi.kt @@ -467,17 +467,21 @@ open class PretixApi(url: String, key: String, orgaSlug: String, version: Int, h throw FinalApiException("Server error: " + response.code + ".") } if (response.code == 401) { - if (body.startsWith("{")) { + val detail = if (body.startsWith("{")) { try { - val err = JSONObject(body) - if (err.optString("detail", "") == "Device access has been revoked.") { - response.close() - throw DeviceAccessRevokedException("Device access has been revoked.") - } + JSONObject(body).optString("detail", "") } catch (e: JSONException) { e.printStackTrace() + "" } + } else { + "" + } + response.close() + if (detail == "Device access has been revoked.") { + throw DeviceAccessRevokedException("Device access has been revoked.") } + throw UnauthorizedApiException(detail.ifEmpty { "Server error: Unauthorized." }) } return try { if (json && response.code != 204) { diff --git a/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/UnauthorizedApiException.java b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/UnauthorizedApiException.java new file mode 100644 index 00000000..33f4dbd8 --- /dev/null +++ b/libpretixsync/src/main/java/eu/pretix/libpretixsync/api/UnauthorizedApiException.java @@ -0,0 +1,9 @@ +package eu.pretix.libpretixsync.api; + +public class UnauthorizedApiException extends FinalApiException { + + public UnauthorizedApiException(String msg) { + super(msg); + } + +} diff --git a/libpretixsync/src/test/java/eu/pretix/libpretixsync/api/PretixApiUnauthorizedTest.kt b/libpretixsync/src/test/java/eu/pretix/libpretixsync/api/PretixApiUnauthorizedTest.kt new file mode 100644 index 00000000..742fb882 --- /dev/null +++ b/libpretixsync/src/test/java/eu/pretix/libpretixsync/api/PretixApiUnauthorizedTest.kt @@ -0,0 +1,104 @@ +package eu.pretix.libpretixsync.api + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSource +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +class PretixApiUnauthorizedTest { + + private class CannedBody(content: String) : ResponseBody() { + private val buffer = Buffer().writeUtf8(content) + var closed = false + + override fun contentType() = "application/json".toMediaType() + + override fun contentLength() = -1L + + override fun source(): BufferedSource = buffer + + override fun close() { + closed = true + super.close() + } + } + + private fun apiResponding(code: Int, body: CannedBody): PretixApi { + val factory = object : HttpClientFactory { + override fun buildClient(ignore_ssl: Boolean): OkHttpClient = + OkHttpClient.Builder() + .addInterceptor { chain -> + Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(code) + .message("Canned") + .body(body) + .build() + } + .build() + } + return PretixApi("https://pretix.example/", "token", "demo", PretixApi.SUPPORTED_API_VERSION, factory) + } + + @Test + fun invalidTokenThrowsUnauthorizedWithServerDetail() { + val body = CannedBody("{\"detail\":\"Invalid token.\"}") + val api = apiResponding(401, body) + + val e = assertThrows(UnauthorizedApiException::class.java) { api.fetchResource(api.apiURL("device/info")) } + + assertEquals("Invalid token.", e.message) + assertTrue(body.closed) + } + + @Test + fun revokedDeviceThrowsDeviceAccessRevoked() { + val body = CannedBody("{\"detail\":\"Device access has been revoked.\"}") + val api = apiResponding(401, body) + + val e = assertThrows(DeviceAccessRevokedException::class.java) { api.fetchResource(api.apiURL("device/info")) } + + assertEquals("Device access has been revoked.", e.message) + assertTrue(body.closed) + } + + @Test + fun unauthorizedWithoutJsonDetailThrowsGenericMessage() { + val body = CannedBody("Unauthorized") + val api = apiResponding(401, body) + + val e = assertThrows(UnauthorizedApiException::class.java) { api.fetchResource(api.apiURL("device/info")) } + + assertEquals("Server error: Unauthorized.", e.message) + assertTrue(body.closed) + } + + @Test + fun unauthorizedDownloadClosesUnreadResponse() { + val body = CannedBody("{\"detail\":\"Invalid token.\"}") + val api = apiResponding(401, body) + + val e = assertThrows(UnauthorizedApiException::class.java) { api.downloadFile(api.apiURL("device/info")) } + + assertEquals("Server error: Unauthorized.", e.message) + assertTrue(body.closed) + } + + @Test + fun successfulResponseReturnsParsedJson() { + val api = apiResponding(200, CannedBody("{\"name\":\"Scanner\"}")) + + val result = api.fetchResource(api.apiURL("device/info")) + + assertEquals(200, result.response.code) + assertEquals("Scanner", result.data!!.getString("name")) + } +}