field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

ComposerPersistenceTests.kt (7062B)


      1 package tera.smoke
      2 
      3 import java.nio.ByteBuffer
      4 import kotlin.test.Test
      5 import kotlin.test.assertEquals
      6 import kotlin.test.assertFailsWith
      7 import kotlin.test.assertFalse
      8 import kotlin.test.assertNotNull
      9 import kotlin.test.assertNull
     10 import kotlin.test.assertTrue
     11 import kotlinx.coroutines.runBlocking
     12 import uniffi.tera_core.FfiAddCommandType
     13 import uniffi.tera_core.FfiComposerFormRecord
     14 import uniffi.tera_core.FfiComposerListEntry
     15 import uniffi.tera_core.FfiComposerMediaRecord
     16 import uniffi.tera_core.FfiComposerRepairReason
     17 import uniffi.tera_core.FfiComposerSaveRequest
     18 import uniffi.tera_core.FfiComposerScopeRecord
     19 import uniffi.tera_core.FfiConverterTypeFfiComposerListEntry
     20 import uniffi.tera_core.FfiConverterTypeFfiComposerSaveReceipt
     21 import uniffi.tera_core.FfiEventTimingKind
     22 import uniffi.tera_core.FfiRecoveryDisposition
     23 import uniffi.tera_core.TeraAppException
     24 import uniffi.tera_core.classifyErrorRecovery
     25 import uniffi.tera_core.composerReserveId
     26 
     27 class ComposerPersistenceTests {
     28     private val scope = FfiComposerScopeRecord(1u, PUBLIC_KEY, "nearby")
     29 
     30     private fun request() = FfiComposerSaveRequest(
     31         schemaVersion = 1u, scope = scope, id = composerReserveId().id,
     32         expectedRevision = null, editSequence = ULong.MAX_VALUE - 1uL,
     33         form = FfiComposerFormRecord(
     34             schemaVersion = 1u,
     35             commandType = FfiAddCommandType.CREATE_EVENT,
     36             content = "  private incomplete\n\u0000é  ",
     37             identifier = null,
     38             title = null,
     39             summary = null,
     40             location = null,
     41             eventTiming = FfiEventTimingKind.ALL_DAY,
     42             eventStartDate = "2026-09-",
     43             eventEndDate = "",
     44             eventStartUnixS = ULong.MAX_VALUE,
     45             eventEndUnixS = null,
     46             eventTimezone = "Mars/unfinished",
     47             priceAmount = "12.",
     48             currency = null,
     49             unit = null,
     50             quantity = "-",
     51             foodPublishedAtUnixS = null,
     52             foodStatus = null,
     53             media = listOf(FfiComposerMediaRecord(1u, "media:abc", "ab".repeat(32), "image/png", 24uL, 2u, 2u, "", UNIX_S)),
     54         ),
     55     )
     56 
     57     @Test
     58     fun actualNativeComposerPreservesPartialFormsExactUnsignedReceiptsAndRestart(): Unit = runBlocking {
     59         SmokeFixture().use { fixture ->
     60             val runtime = fixture.runtime()
     61             val input = request()
     62             val saved = try {
     63                 val first = runtime.composerSave(input)
     64                 assertEquals(1uL, first.draft.revision)
     65                 assertEquals(scope, first.draft.scope)
     66                 assertEquals(input.id, first.draft.id)
     67                 assertEquals(input.form, first.draft.form)
     68                 assertEquals(ULong.MAX_VALUE - 1uL, first.draft.editSequence)
     69                 assertFalse(first.replayed)
     70                 val next = input.copy(expectedRevision = 1uL, editSequence = ULong.MAX_VALUE,
     71                     form = input.form.copy(content = "newest incomplete"))
     72                 val receipt = runtime.composerSave(next)
     73                 assertEquals(2uL, receipt.draft.revision)
     74                 assertEquals(ULong.MAX_VALUE, receipt.draft.editSequence)
     75                 assertEquals(next.form, receipt.draft.form)
     76                 assertEquals(receipt, FfiConverterTypeFfiComposerSaveReceipt.lift(
     77                     FfiConverterTypeFfiComposerSaveReceipt.lower(receipt)))
     78                 val stale = assertFailsWith<TeraAppException.Failure> { runtime.composerSave(next) }
     79                 assertEquals("composer_revision_conflict", stale.report.code)
     80                 assertEquals(listOf("reload_composer"), stale.report.recoveryActions)
     81                 assertEquals(FfiRecoveryDisposition.STALE_REVISION, classifyErrorRecovery(1u, stale.report.code).disposition)
     82                 assertTrue(runtime.phase1DraftHeads(100u).isEmpty())
     83                 receipt
     84             } finally {
     85                 runtime.shutdown()
     86                 runtime.close()
     87             }
     88             val reopened = fixture.runtime()
     89             try {
     90                 assertEquals(saved.draft, reopened.composerLoad(scope, input.id))
     91                 val second = reopened.composerSave(request())
     92                 val page = reopened.composerList(1u, scope, 1u, null)
     93                 assertEquals(scope, page.scope)
     94                 assertEquals(1, page.entries.size)
     95                 val cursor = assertNotNull(page.nextCursor)
     96                 val last = reopened.composerList(1u, scope, 1u, cursor)
     97                 assertNull(last.nextCursor)
     98                 assertEquals(setOf(input.id, second.draft.id), (page.entries + last.entries).map {
     99                     (it as FfiComposerListEntry.Draft).summary.id
    100                 }.toSet())
    101                 val foreign = scope.copy(localNetworkId = "elsewhere")
    102                 assertEquals("composer_scope_mismatch",
    103                     assertFailsWith<TeraAppException.Failure> { reopened.composerLoad(foreign, input.id) }.report.code)
    104                 assertEquals("composer_scope_mismatch",
    105                     assertFailsWith<TeraAppException.Failure> { reopened.composerList(1u, foreign, 1u, cursor) }.report.code)
    106                 assertTrue(reopened.composerList(1u, foreign, 10u, null).entries.isEmpty())
    107             } finally {
    108                 reopened.shutdown()
    109                 reopened.close()
    110             }
    111         }
    112     }
    113 
    114     @Test
    115     fun unsupportedNestedVersionsAndMalformedVariantsFailClosed(): Unit = runBlocking {
    116         SmokeFixture().use { fixture ->
    117             val runtime = fixture.runtime()
    118             try {
    119                 val input = request()
    120                 for (invalid in listOf(
    121                     input.copy(schemaVersion = 2u),
    122                     input.copy(scope = scope.copy(schemaVersion = 2u)),
    123                     input.copy(form = input.form.copy(schemaVersion = 2u)),
    124                     input.copy(form = input.form.copy(media = listOf(input.form.media[0].copy(schemaVersion = 2u)))),
    125                 )) {
    126                     val failure = assertFailsWith<TeraAppException.Failure> { runtime.composerSave(invalid) }
    127                     assertEquals(1u.toUShort(), failure.report.schemaVersion)
    128                     assertEquals("composer_schema_unsupported", failure.report.code)
    129                     assertFalse(failure.report.retryable)
    130                 }
    131                 assertTrue(runtime.composerList(1u, scope, 10u, null).entries.isEmpty())
    132                 val repair = FfiComposerListEntry.Repair("00".repeat(16), ULong.MAX_VALUE, FfiComposerRepairReason.CORRUPT_RECORD)
    133                 assertEquals(repair, FfiConverterTypeFfiComposerListEntry.lift(FfiConverterTypeFfiComposerListEntry.lower(repair)))
    134                 for (bytes in listOf(byteArrayOf(), byteArrayOf(0, 0, 0, 99), byteArrayOf(0, 0, 0, 1))) {
    135                     assertFailsWith<RuntimeException> { FfiConverterTypeFfiComposerListEntry.read(ByteBuffer.wrap(bytes)) }
    136                 }
    137             } finally {
    138                 runtime.shutdown()
    139                 runtime.close()
    140             }
    141         }
    142     }
    143 }