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
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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`.
|
* 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,
|
* id, name, flow ("pkce" | "device_code" | "external"), cli_command,
|
||||||
* docs_url, disconnect_hint, disconnect_command, disconnectable, status.
|
* 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
|
@Serializable
|
||||||
data class OAuthProvider(
|
data class OAuthProvider(
|
||||||
@@ -31,9 +42,13 @@ data class OAuthProviderStatus(
|
|||||||
val source: String? = null,
|
val source: String? = null,
|
||||||
@SerialName("source_label") val sourceLabel: String? = null,
|
@SerialName("source_label") val sourceLabel: String? = null,
|
||||||
@SerialName("token_preview") val tokenPreview: 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("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,
|
val error: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,3 +113,25 @@ data class OAuthCancelResponse(
|
|||||||
@SerialName("session_id") val sessionId: String? = null,
|
@SerialName("session_id") val sessionId: String? = null,
|
||||||
val message: 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())
|
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
|
@Test
|
||||||
fun getOAuthProviders_parsesList() =
|
fun getOAuthProviders_parsesList() =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
|||||||
Reference in new issue
Block a user