field_ios

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

TeraSubmissionStoreTests.swift (18741B)


      1 import Foundation
      2 @testable import TeraApp
      3 import XCTest
      4 
      5 @MainActor
      6 final class TeraSubmissionStoreTests: XCTestCase {
      7   func testTerminalOperationsReconcileReceiptsWithoutOpeningOrUploadingUnfinishedMedia() async throws {
      8     for state in [TeraOutboxState.complete, .terminal, .cancelled] {
      9       let backend = AddBackend()
     10       let client = try await TeraAddStoreTests.startedClient(backend)
     11       let store = await make(client, backend: backend)
     12       store.updateForm(\.content, "original")
     13       await store.submit()
     14       let original = try XCTUnwrap(store.submissions.status)
     15       let media = AddMediaHarness(failOpen: true)
     16       let item = await media.captureImage()
     17       var capturedForm = original.captured.form.editingValue
     18       capturedForm.media = [item]
     19       let captured = TeraComposerDraft(scope: original.captured.scope, id: original.captured.id,
     20                                        revision: original.captured.revision, editSequence: original.captured.editSequence,
     21                                        form: TeraComposerForm(editing: capturedForm))
     22       let status = TeraSubmissionStatus(
     23         request: original.request, intentID: original.intentID, operationID: original.operationID,
     24         revision: original.revision, captured: captured, state: state,
     25         committedAtUnixMilliseconds: original.committedAtUnixMilliseconds,
     26         updatedAtUnixMilliseconds: original.updatedAtUnixMilliseconds,
     27         media: [TeraSubmissionMedia(opaqueReference: item.opaqueReference, progress: TeraDraftMediaStatus(
     28           url: "https://blossom.example/\(item.sha256).png", stage: .pending, uploadAttempts: 0,
     29           verifiedAtUnixMilliseconds: nil, possibleOrphan: false, orphanReasonCode: nil, orphanRecordedAtUnixMilliseconds: nil
     30         ))], settlement: original.settlement, delivery: original.delivery, targetDetails: original.targetDetails, retry: original.retry
     31       )
     32       let before = await backend.submissionBackend.advanceCount
     33       let effects = TeraSubmissionEffects(client: client, media: media, ensure: {},
     34                                           accept: { _ in XCTFail("Terminal work cannot advance") })
     35       do { try await effects.advance(status) } catch { XCTFail("Terminal reconciliation must not open media: \(state)") }
     36       let opened = await media.openCount
     37       let reconciled = await media.submissionReconciliations
     38       let uploads = await backend.submissionBackend.uploadCount
     39       let after = await backend.submissionBackend.advanceCount
     40       XCTAssertEqual(opened, 0)
     41       XCTAssertEqual(uploads, 0)
     42       XCTAssertEqual(after, before)
     43       XCTAssertEqual(reconciled, 1)
     44       store.stop()
     45       _ = try await client.stop()
     46     }
     47   }
     48 
     49   func testDoubleTapAndTenThousandEditsKeepOneCapturedRequestDuringSlowPrepare() async throws {
     50     let backend = AddBackend()
     51     let client = try await TeraAddStoreTests.startedClient(backend)
     52     let store = await make(client, backend: backend)
     53     let pause = ResourceTestPause()
     54     await backend.submissionBackend.pausePrepare(pause)
     55     store.updateForm(\.content, "The tap's original text")
     56     let submit = Task { await store.submit() }
     57     await entered(pause)
     58     let original = try XCTUnwrap(store.submissions.request)
     59     XCTAssertTrue(store.submissions.isWorking)
     60     XCTAssertFalse(store.isWorking)
     61     XCTAssertTrue(store.isFormEditable)
     62     for index in 1 ... 10000 {
     63       store.updateForm(\.content, "later \(index)")
     64     }
     65     await store.submit()
     66     XCTAssertEqual(store.submissions.request, original)
     67     XCTAssertEqual(store.form.content, "later 10000")
     68     let captured = try await client.loadComposer(scope: original.scope, id: original.composerID)
     69     XCTAssertEqual(captured.form.content, "The tap's original text")
     70     XCTAssertEqual(captured.revision, original.expectedRevision)
     71     XCTAssertTrue(store.canSave)
     72     await store.save()
     73     XCTAssertEqual(store.savedComposer?.form.content, "later 10000")
     74     XCTAssertNotEqual(store.savedComposer?.id, original.composerID)
     75     XCTAssertEqual(store.composerState, .saved)
     76     // Navigation suspends observation without abandoning the active operation.
     77     store.suspend()
     78     await pause.resume.open()
     79     await submit.value
     80     await store.start()
     81     await store.save()
     82     XCTAssertEqual(store.submissions.status?.state, .complete)
     83     XCTAssertEqual(store.submissions.status?.captured.form.content, "The tap's original text")
     84     XCTAssertEqual(store.savedComposer?.form.content, "later 10000")
     85     XCTAssertEqual(store.form.content, "later 10000")
     86     let counts = await (backend.submissionBackend.idCount, backend.submissionBackend.prepareCount)
     87     XCTAssertEqual(counts.0, 1)
     88     XCTAssertEqual(counts.1, 1)
     89     _ = try await client.stop()
     90   }
     91 
     92   func testCancellingViewWaiterDoesNotCancelOrReplaceTheOperation() async throws {
     93     let backend = AddBackend()
     94     let client = try await TeraAddStoreTests.startedClient(backend)
     95     let store = await make(client, backend: backend)
     96     let pause = ResourceTestPause()
     97     await backend.submissionBackend.pauseAdvance(pause)
     98     store.updateForm(\.content, "original")
     99     let submit = Task { await store.submit() }
    100     await entered(pause)
    101     submit.cancel()
    102     store.updateForm(\.content, "newer")
    103     await store.submit()
    104     XCTAssertTrue(store.submissions.isWorking)
    105     await pause.resume.open()
    106     await submit.value
    107     XCTAssertEqual(store.submissions.status?.state, .complete)
    108     XCTAssertEqual(store.submissions.status?.captured.form.content, "original")
    109     XCTAssertEqual(store.form.content, "newer")
    110     let count = await backend.submissionBackend.prepareCount
    111     XCTAssertEqual(count, 1)
    112     _ = try await client.stop()
    113   }
    114 
    115   func testExplicitStopWaitKeepsWorkerSlotAndRecoversLateCommitWithOriginalID() async throws {
    116     let backend = AddBackend()
    117     let client = try await TeraAddStoreTests.startedClient(backend)
    118     let store = await make(client, backend: backend)
    119     let pause = ResourceTestPause()
    120     await backend.submissionBackend.pausePrepare(pause)
    121     store.updateForm(\.content, "original")
    122     let submit = Task { await store.submit() }
    123     await entered(pause)
    124     let request = try XCTUnwrap(store.submissions.request)
    125     store.submissions.stopWaiting()
    126     XCTAssertTrue(store.submissions.isWorking)
    127     await store.submit()
    128     store.updateForm(\.content, "later")
    129     await pause.resume.open()
    130     await submit.value
    131     XCTAssertNil(store.submissions.status)
    132     XCTAssertEqual(store.submissions.request, request)
    133     XCTAssertFalse(store.submissions.isWorking)
    134     await store.submit()
    135     XCTAssertEqual(store.submissions.request, request)
    136     XCTAssertEqual(store.submissions.status?.state, .complete)
    137     XCTAssertEqual(store.submissions.status?.captured.form.content, "original")
    138     XCTAssertEqual(store.form.content, "later")
    139     let count = await backend.submissionBackend.prepareCount
    140     XCTAssertEqual(count, 1)
    141     _ = try await client.stop()
    142   }
    143 
    144   func testLostCommitAndUnreadableRecoveryCannotTurnNewerEditingIntoAReplacementRequest() async throws {
    145     let backend = AddBackend()
    146     let client = try await TeraAddStoreTests.startedClient(backend)
    147     let store = await make(client, backend: backend)
    148     let pause = ResourceTestPause()
    149     await backend.submissionBackend.pausePrepare(pause, loseReceipt: true)
    150     store.updateForm(\.content, "original")
    151     let submit = Task { await store.submit() }
    152     await entered(pause)
    153     let request = try XCTUnwrap(store.submissions.request)
    154     await backend.submissionBackend.unreadable(true)
    155     store.updateForm(\.content, "newer editing")
    156     await pause.resume.open()
    157     await submit.value
    158     await store.submit()
    159     XCTAssertEqual(store.submissions.request, request)
    160     XCTAssertNil(store.submissions.status)
    161     let captured = try await client.loadComposer(scope: request.scope, id: request.composerID)
    162     XCTAssertEqual(captured.form.content, "original")
    163     XCTAssertEqual(captured.revision, request.expectedRevision)
    164     await store.save()
    165     XCTAssertEqual(store.savedComposer?.form.content, "newer editing")
    166     XCTAssertNotEqual(store.savedComposer?.id, request.composerID)
    167     XCTAssertEqual(store.form.content, "newer editing")
    168     await backend.submissionBackend.unreadable(false)
    169     await store.submit()
    170     XCTAssertEqual(store.submissions.status?.request, request)
    171     XCTAssertEqual(store.submissions.status?.captured.form.content, "original")
    172     let count = await backend.submissionBackend.prepareCount
    173     XCTAssertEqual(count, 1)
    174     _ = try await client.stop()
    175   }
    176 
    177   func testRecreatedStoreSelectsSavedOperationWithoutChangingEditingOrBeginningEffects() async throws {
    178     let backend = AddBackend(advanceOffline: true)
    179     let client = try await TeraAddStoreTests.startedClient(backend)
    180     let first = await make(client, backend: backend)
    181     first.updateForm(\.content, "original")
    182     await first.submit()
    183     let saved = try XCTUnwrap(first.submissions.status)
    184     first.stop()
    185     let replacement = await make(client, backend: backend)
    186     replacement.updateForm(\.content, "reopened editing")
    187     await TeraScopeFixtures.eventually { !replacement.submissions.inventory.isLoading }
    188     guard case let .submission(summary) = replacement.submissions.inventory.entries.first else {
    189       return XCTFail("Original operation must be selectable after relaunch")
    190     }
    191     let before = await backend.submissionBackend.advanceCount
    192     await replacement.submissions.select(summary)
    193     let after = await backend.submissionBackend.advanceCount
    194     XCTAssertEqual(after, before)
    195     XCTAssertEqual(replacement.submissions.status, saved)
    196     XCTAssertEqual(replacement.form.content, "reopened editing")
    197     await replacement.submissions.refreshSelected()
    198     let checked = await backend.submissionBackend.advanceCount
    199     XCTAssertEqual(checked, before, "Checking status must not begin a publication effect")
    200     await replacement.submissions.continueSelected()
    201     XCTAssertEqual(replacement.submissions.status?.operationID, saved.operationID)
    202     XCTAssertEqual(replacement.submissions.request, saved.request)
    203     XCTAssertEqual(replacement.form.content, "reopened editing")
    204     _ = try await client.stop()
    205   }
    206 
    207   func testExplicitNewMakesDistinctOperationForIdenticalIntentionalContent() async throws {
    208     let backend = AddBackend()
    209     let client = try await TeraAddStoreTests.startedClient(backend)
    210     let store = await make(client, backend: backend)
    211     store.updateForm(\.content, "identical")
    212     await store.submit()
    213     let first = try XCTUnwrap(store.submissions.status)
    214     await store.submit()
    215     XCTAssertEqual(store.submissions.status, first)
    216     store.newDraft()
    217     await TeraScopeFixtures.eventually { !store.protection.isWorking }
    218     store.updateForm(\.content, "identical")
    219     await store.submit()
    220     let second = try XCTUnwrap(store.submissions.status)
    221     XCTAssertNotEqual(second.request.commandID, first.request.commandID)
    222     XCTAssertNotEqual(second.operationID, first.operationID)
    223     XCTAssertEqual(second.captured.form, first.captured.form)
    224     let count = await backend.submissionBackend.prepareCount
    225     XCTAssertEqual(count, 2)
    226     _ = try await client.stop()
    227   }
    228 
    229   private func make(_ client: TeraRuntimeClient, backend: AddBackend) async -> TeraAddStore {
    230     let store = TeraAddStore(runtimeClient: client)
    231     await store.configure(snapshot: backend.snapshot())
    232     await store.start()
    233     return store
    234   }
    235 
    236   private func entered(_ pause: ResourceTestPause) async {
    237     let reached = expectation(description: "Submission reached controlled native boundary")
    238     let observer = Task { await pause.entered.wait(); reached.fulfill() }
    239     await fulfillment(of: [reached], timeout: 3)
    240     await pause.entered.open()
    241     await observer.value
    242   }
    243 }
    244 
    245 extension TeraSubmissionStoreTests {
    246   func testLateAcceptedPublicationCannotPopulateAReplacementAccountOrNetworkScope() async throws {
    247     for replacingAccount in [false, true] {
    248       let backend = AddBackend()
    249       let client = try await TeraAddStoreTests.startedClient(backend)
    250       let store = await make(client, backend: backend)
    251       let pause = ResourceTestPause()
    252       await backend.submissionBackend.pauseAdvance(pause)
    253       store.updateForm(\.content, "Original publication")
    254       let task = Task { await store.submit() }
    255       await entered(pause)
    256       let original = try XCTUnwrap(store.submissions.status)
    257       let replacement = TeraComposerScope(
    258         authorPublicKey: replacingAccount ? String(repeating: "cd", count: 32) : original.request.scope.authorPublicKey,
    259         localNetworkID: replacingAccount ? original.request.scope.localNetworkID : "replacement-network"
    260       )
    261       let context = TeraLocalNetwork(schemaVersion: 1, id: replacement.localNetworkID, label: "Replacement",
    262                                      relayURLs: [], locality: nil, followedAuthors: [], generation: 2)
    263       store.submissions.configure(scope: replacement, context: context)
    264       store.submissions.start()
    265       await pause.resume.open()
    266       await task.value
    267       var recovered: TeraSubmissionStatus?
    268       let deadline = ContinuousClock.now.advanced(by: .seconds(3))
    269       repeat {
    270         recovered = try? await client.submissionStatus(request: original.request)
    271         if recovered?.delivery.state == .accepted {
    272           break
    273         }
    274         try await Task.sleep(for: .milliseconds(1))
    275       } while ContinuousClock.now < deadline
    276       let accepted = try XCTUnwrap(recovered)
    277       XCTAssertEqual(accepted.delivery.state, .accepted)
    278       XCTAssertEqual(accepted.request, original.request)
    279       XCTAssertEqual(accepted.operationID, original.operationID)
    280       XCTAssertEqual(accepted.captured, original.captured)
    281       XCTAssertNil(store.submissions.request)
    282       XCTAssertNil(store.submissions.status)
    283       XCTAssertNil(store.submissions.message)
    284       XCTAssertNil(store.submissions.failureCode)
    285       let advances = await backend.submissionBackend.advanceCount
    286       XCTAssertEqual(advances, 1)
    287       store.stop()
    288       _ = try await client.stop()
    289     }
    290   }
    291 
    292   func testStopBeforeIntentCommitStaysUnconfirmedUntilOriginalPreparationReturns() async throws {
    293     let backend = AddBackend()
    294     let client = try await TeraAddStoreTests.startedClient(backend)
    295     let pause = ResourceTestPause()
    296     let media = AddMediaHarness(openPause: pause)
    297     let store = TeraAddStore(runtimeClient: client, media: media)
    298     await store.configure(snapshot: backend.snapshot())
    299     await store.start()
    300     store.selectType(.createPhotoUpdate)
    301     store.updateForm(\.content, "Stop before commit")
    302     await store.importPhotos()
    303     let task = Task { await store.submit() }
    304     await entered(pause)
    305     let original = try XCTUnwrap(store.submissions.request)
    306     await store.submissions.requestStop()
    307     XCTAssertNil(store.submissions.status)
    308     XCTAssertTrue(store.submissions.message?.contains("not yet confirmed") == true)
    309     await pause.resume.open()
    310     await task.value
    311     XCTAssertTrue(store.submissions.status?.delivery.isStopped == true)
    312     XCTAssertEqual(store.submissions.request, original)
    313     let prepared = await backend.submissionBackend.prepareCount
    314     let uploads = await backend.submissionBackend.uploadCount
    315     let advances = await backend.submissionBackend.advanceCount
    316     XCTAssertEqual(prepared, 1)
    317     XCTAssertEqual(uploads + advances, 0)
    318     store.stop()
    319     _ = try await client.stop()
    320   }
    321 
    322   func testStopWhilePrepareCallbackIsHeldRetainsStopAndDoesNotAdvanceOldCallback() async throws {
    323     let backend = AddBackend()
    324     let client = try await TeraAddStoreTests.startedClient(backend)
    325     let store = await make(client, backend: backend)
    326     let pause = ResourceTestPause()
    327     await backend.submissionBackend.pausePrepare(pause)
    328     store.updateForm(\.content, "Original stop request")
    329     let task = Task { await store.submit() }
    330     await entered(pause)
    331     await store.submissions.requestStop()
    332     let stopped = try XCTUnwrap(store.submissions.status)
    333     XCTAssertTrue(stopped.delivery.isStopped)
    334     XCTAssertTrue(store.submissions.isWorking)
    335     await store.submissions.requestStop()
    336     XCTAssertEqual(store.submissions.status, stopped)
    337     await pause.resume.open()
    338     await task.value
    339     XCTAssertEqual(store.submissions.status, stopped)
    340     let advances = await backend.submissionBackend.advanceCount
    341     XCTAssertEqual(advances, 0)
    342     await store.submit()
    343     XCTAssertEqual(store.submissions.status, stopped)
    344     let prepared = await backend.submissionBackend.prepareCount
    345     XCTAssertEqual(prepared, 1)
    346     store.stop()
    347     _ = try await client.stop()
    348   }
    349 
    350   func testStopRemainsVisibleWhenAlreadyIssuedWorkReturnsAccepted() async throws {
    351     let backend = AddBackend()
    352     let client = try await TeraAddStoreTests.startedClient(backend)
    353     let store = await make(client, backend: backend)
    354     let pause = ResourceTestPause()
    355     await backend.submissionBackend.pauseAdvance(pause)
    356     store.updateForm(\.content, "Already issued")
    357     let task = Task { await store.submit() }
    358     await entered(pause)
    359     await store.submissions.requestStop()
    360     let stopped = try XCTUnwrap(store.submissions.status)
    361     XCTAssertTrue(stopped.delivery.isStopped)
    362     await pause.resume.open()
    363     await task.value
    364     let accepted = try XCTUnwrap(store.submissions.status)
    365     XCTAssertEqual(accepted.delivery.state, .accepted)
    366     XCTAssertEqual(accepted.delivery.stopRequestedAtUnixMilliseconds, stopped.delivery.stopRequestedAtUnixMilliseconds)
    367     XCTAssertEqual(accepted.operationID, stopped.operationID)
    368     XCTAssertTrue(accepted.summary.contains("accepted"))
    369     let advances = await backend.submissionBackend.advanceCount
    370     XCTAssertEqual(advances, 1)
    371     store.stop()
    372     _ = try await client.stop()
    373   }
    374 
    375   func testUnconfirmedStopDoesNotClaimPersistenceAndRetryUsesOriginalRequest() async throws {
    376     let backend = AddBackend()
    377     let client = try await TeraAddStoreTests.startedClient(backend)
    378     let store = await make(client, backend: backend)
    379     store.updateForm(\.content, "Original")
    380     await store.submit()
    381     let original = try XCTUnwrap(store.submissions.status)
    382     await backend.submissionBackend.unreadable(true)
    383     await store.submissions.requestStop()
    384     XCTAssertEqual(store.submissions.status, original)
    385     XCTAssertTrue(store.submissions.message?.contains("not yet confirmed") == true)
    386     await backend.submissionBackend.unreadable(false)
    387     await store.submissions.refreshSelected()
    388     XCTAssertTrue(store.submissions.status?.delivery.isStopped == true)
    389     XCTAssertEqual(store.submissions.request, original.request)
    390     let advances = await backend.submissionBackend.advanceCount
    391     XCTAssertEqual(advances, 1)
    392     store.stop()
    393     _ = try await client.stop()
    394   }
    395 }