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
@@ -0,0 +1,68 @@
package com.tt.honeyDue.network
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.models.RegisterRequest
import com.tt.honeyDue.models.VerifyEmailRequest
import kotlinx.coroutines.runBlocking
import org.junit.Assume.assumeTrue
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* LIVE end-to-end integration test: drives the REAL [AuthApi] (the exact client
* code the app runs, OkHttp engine, production URLs from ApiConfig) against the
* live honeyDue API + Ory Kratos. No fixtures, no hand-written JSON — every
* response is decoded by the real models over the wire, so a contract mismatch
* between the API and the KMP client fails here.
*
* Skipped unless RUN_LIVE_IT=1 (it creates a real throwaway account and needs
* network), so it never runs on a normal build/CI. Run explicitly:
* RUN_LIVE_IT=1 ./gradlew :composeApp:testDebugUnitTest \
* --tests "com.tt.honeyDue.network.LiveAuthIntegrationTest"
*/
class LiveAuthIntegrationTest {
private fun liveEnabled() = System.getenv("RUN_LIVE_IT") == "1"
@Test
fun passwordSignupFlowAgainstProd() = runBlocking {
assumeTrue("set RUN_LIVE_IT=1 to run the live integration test", liveEnabled())
val api = AuthApi() // ApiClient.httpClient (OkHttp) + PROD URLs
val email = "kmpqa-" + System.currentTimeMillis() + "@example.com"
val password = "KmpQa1!Test99"
// 1. Register: admin-create via /api/auth/register/ then immediate login.
// Exercises the createAccount HTTP call AND the login decode that
// crashed on "continue_with": null.
val reg = api.register(
RegisterRequest(username = "kmpqa", email = email, password = password, firstName = "Kmp", lastName = "Qa"),
)
assertTrue(reg is ApiResult.Success, "register should succeed, got: $reg")
val auth = (reg as ApiResult.Success).data
assertTrue(auth.token.isNotBlank(), "session token must be present")
assertEquals(email, auth.user.email)
assertFalse(auth.user.verified, "a fresh password account must start unverified")
// 2. /auth/me decodes through the ApiClient json config (User model).
val me = api.getCurrentUser(auth.token)
assertTrue(me is ApiResult.Success, "auth/me should succeed, got: $me")
assertEquals(email, (me as ApiResult.Success).data.email)
// 3. Start the client-owned verification flow — decodes the flow body and
// stores the flow id; sends the single code.
val started = api.startEmailVerification(email)
assertTrue(started is ApiResult.Success, "startEmailVerification should succeed, got: $started")
assertNotNull(DataManager.pendingVerificationFlowId.value, "a verification flow id must be stored")
// 4. Submit a deliberately wrong code: must decode the re-rendered flow
// and return a clean Error (not a parse crash).
val wrong = api.verifyEmail(auth.token, VerifyEmailRequest(code = "000000"))
assertTrue(wrong is ApiResult.Error, "wrong code must return Error, got: $wrong")
DataManager.setPendingVerificationFlowId(null)
}
}