fix(providers): accept numeric expires_at in OAuth status (#1407)
This commit is contained in:
2 files changed
+61
-3
No files matched your search
@@ -1,7 +1,17 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
/**
|
||||
* OAuth provider catalog entry returned by `GET /api/providers/oauth`.
|
||||
@@ -10,7 +20,8 @@ import kotlinx.serialization.Serializable
|
||||
* id, name, flow ("pkce" | "device_code" | "external"), cli_command,
|
||||
* docs_url, disconnect_hint, disconnect_command, disconnectable, status.
|
||||
*
|
||||
* `expires_at` in the status comes back as an ISO timestamp string.
|
||||
* `expires_at` / `last_refresh` come back as an ISO string or a numeric epoch depending on the
|
||||
* credential source, so they decode through [LenientStringSerializer].
|
||||
*/
|
||||
@Serializable
|
||||
data class OAuthProvider(
|
||||
@@ -31,9 +42,13 @@ data class OAuthProviderStatus(
|
||||
val source: String? = null,
|
||||
@SerialName("source_label") val sourceLabel: String? = null,
|
||||
@SerialName("token_preview") val tokenPreview: String? = null,
|
||||
@SerialName("expires_at") val expiresAt: String? = null,
|
||||
@SerialName("expires_at")
|
||||
@Serializable(with = LenientStringSerializer::class)
|
||||
val expiresAt: String? = null,
|
||||
@SerialName("has_refresh_token") val hasRefreshToken: Boolean? = null,
|
||||
@SerialName("last_refresh") val lastRefresh: String? = null,
|
||||
@SerialName("last_refresh")
|
||||
@Serializable(with = LenientStringSerializer::class)
|
||||
val lastRefresh: String? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@@ -98,3 +113,25 @@ data class OAuthCancelResponse(
|
||||
@SerialName("session_id") val sessionId: String? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
/** Decodes a JSON string or number (e.g. epoch millis) into its string form; null stays null. */
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
object LenientStringSerializer : KSerializer<String?> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("LenientString", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): String? {
|
||||
if (decoder is JsonDecoder) {
|
||||
val element = decoder.decodeJsonElement()
|
||||
return if (element is JsonPrimitive && element !is JsonNull) element.content else null
|
||||
}
|
||||
return decoder.decodeString()
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: String?,
|
||||
) {
|
||||
if (value == null) encoder.encodeNull() else encoder.encodeString(value)
|
||||
}
|
||||
}
|
||||
+21
@@ -598,6 +598,27 @@ class HermesApiServiceMockWebServerTest {
|
||||
assertEquals(404, response.code())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getOAuthProviders_toleratesNumericExpiresAt() =
|
||||
runBlocking {
|
||||
mockServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody(
|
||||
"""
|
||||
{"providers":[{"id":"codex","name":"Codex","flow":"external",
|
||||
"status":{"logged_in":true,"expires_at":1790766533634,"last_refresh":1790766000000}}]}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
val response = api.getOAuthProviders()
|
||||
assertTrue(response.isSuccessful)
|
||||
val status = response.body()!!.providers[0].status
|
||||
assertEquals("1790766533634", status.expiresAt)
|
||||
assertEquals("1790766000000", status.lastRefresh)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getOAuthProviders_parsesList() =
|
||||
runBlocking {
|
||||
|
||||
Reference in new issue
Block a user