Fix continue_with:null decode crash + add auth decode/integration tests
Android UI Tests / ui-tests (push) Has been cancelled

Kratos serialises an empty `continue_with` as explicit `null` (not `[]` or an
absent key), which crashed the post-register login decode ("Expected start of
the array '[', but had 'n' at $.continue_with"). Make continue_with nullable on
the three Kratos models and add coerceInputValues as a backstop for other
null-vs-default fields.

Tests (all run + passing):
- KratosDecodeTest: null/absent continue_with on login + registration
- AuthFlowDecodeTest: real captured prod bodies (login, /auth/me, verification)
  decoded with the real models + the real client Json configs
- LiveAuthIntegrationTest: live HTTP through the actual AuthApi against prod
  (register -> login -> /auth/me -> start-verification -> wrong-code), gated by
  RUN_LIVE_IT=1 so it never runs on a normal build

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-06-03 22:30:48 -05:00
parent 7c892d2bb6
commit 6058013951
6 changed files with 350 additions and 5 deletions
@@ -39,8 +39,13 @@ data class KratosFlow(
* Post-submission instructions. On a completed recovery flow this carries
* the privileged session token (`set_ory_session_token`) and the settings
* flow to finish the password change in (`show_settings_ui`).
*
* Nullable because Kratos serialises this as an explicit `"continue_with":
* null` (not an empty array / absent key) when there are no items — a
* non-nullable List would throw on decode ("Expected start of the array
* '[', but had 'n'").
*/
@SerialName("continue_with") val continueWith: List<KratosContinueWith> = emptyList(),
@SerialName("continue_with") val continueWith: List<KratosContinueWith>? = null,
)
/**
@@ -149,7 +154,8 @@ data class KratosSession(
data class KratosLoginSuccess(
val session: KratosSession,
@SerialName("session_token") val sessionToken: String,
@SerialName("continue_with") val continueWith: List<KratosContinueWith> = emptyList(),
// Nullable: Kratos sends "continue_with": null on a plain login.
@SerialName("continue_with") val continueWith: List<KratosContinueWith>? = null,
)
/**
@@ -162,7 +168,8 @@ data class KratosRegistrationSuccess(
val session: KratosSession? = null,
@SerialName("session_token") val sessionToken: String? = null,
val identity: KratosIdentity? = null,
@SerialName("continue_with") val continueWith: List<KratosContinueWith> = emptyList(),
// Nullable: Kratos may serialise this as an explicit null.
@SerialName("continue_with") val continueWith: List<KratosContinueWith>? = null,
)
/**
@@ -49,6 +49,11 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = true
// Kratos serialises empty list/object fields as explicit `null` rather
// than omitting them or sending `[]`. Without this, a non-nullable field
// with a default (e.g. ui.nodes, ui.messages) throws on a null value.
// coerceInputValues falls back to the declared default in that case.
coerceInputValues = true
}
/**
@@ -69,9 +74,9 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
* is rejected. [verifyEmail] reads the persisted id and posts to it
* directly.
*/
private fun capturePendingVerificationFlow(continueWith: List<KratosContinueWith>) {
private fun capturePendingVerificationFlow(continueWith: List<KratosContinueWith>?) {
val flowId = continueWith
.firstOrNull { it.action == "show_verification_ui" }
?.firstOrNull { it.action == "show_verification_ui" }
?.flow?.id
if (!flowId.isNullOrBlank()) {
DataManager.setPendingVerificationFlowId(flowId)