diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponse.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponse.kt index 1296f1a8..f4cfb6c2 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponse.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponse.kt @@ -86,9 +86,17 @@ object WavelogResponse { if (lower.contains("\"status\"") && SUCCESS_MARKERS.none { lower.contains(it) }) { return Verdict.Unreadable(text.take(MAX_DETAIL)) } + // A bulk reply that stored nothing is not an acceptance, whatever its status says. Look4Sat + // posts one QSO per request so this is latent today, but `imported:0` reading as success + // would silently clear the queue if that ever changes. + if (importedZero(lower)) return Verdict.Rejected("server imported nothing") return Verdict.Accepted(text.take(MAX_DETAIL)) } + /** Whether a bulk reply reports that no record was stored. */ + private fun importedZero(lower: String): Boolean = + IMPORT_COUNT_KEYS.any { lower.contains("\"" + it + "\":0") } + /** * The server's own explanation, when it gave one. * @@ -146,5 +154,8 @@ object WavelogResponse { /** Whitespace next to a colon or comma, which JSON allows and servers use inconsistently. */ private val AROUND_SEPARATORS = Regex("""\s*(?=[:,])|(?<=[:,])\s*""") + /** Count fields a bulk reply uses to say how many records it stored. */ + private val IMPORT_COUNT_KEYS = listOf("imported", "adif_count") + private val REASON_KEYS = listOf("reason", "message", "error") } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt index 61581cf9..24f5bb14 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogUploader.kt @@ -76,21 +76,23 @@ class WavelogUploader( // 3. Upload one by one. ADIF gridsquare = counterpart grid (the QSO partner); blank until the scraper lands val entries = queue.all() - var ok = 0 - var fail = 0 + var uploaded = 0 + var failed = 0 var firstError = "" for (qso in entries) { - if (qso.uploaded) { ok++; continue } + // Entries already confirmed by the server are skipped, and NOT counted: adding them to + // the total made a re-run report "N uploaded" for QSOs that went up days ago. + if (qso.uploaded) continue val result = WaveLogApi.postQso(url, apiKey, stationId, qso, qso.gridsquare) if (result is WavelogResult.Success) { - ok++ + uploaded++ queue.markUploaded(qso.id) } else { - fail++ + failed++ if (firstError.isBlank()) firstError = (result as? WavelogResult.Failure)?.message ?: "" } } - return UploadOutcome.Done(ok, fail, UploadOutcome.Reason.COMPLETED, firstError) + return UploadOutcome.Done(uploaded, failed, UploadOutcome.Reason.COMPLETED, firstError) } private suspend fun getStationGrid(url: String, apiKey: String, stationId: String): String? { diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponseTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponseTest.kt index b5a9ece7..1fbcf4e0 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponseTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/wavelog/WavelogResponseTest.kt @@ -220,6 +220,36 @@ class WavelogResponseTest { assertTrue("got " + verdict, verdict is WavelogResponse.Verdict.Rejected) } + /** A bulk reply that stored nothing is not an acceptance, whatever its status says. */ + @Test + fun `a reply that imported nothing is a rejection`() { + for (body in listOf("""{"imported":0,"skipped":3}""", + """{"status":"created","adif_count":0,"adif_errors":1,"messages":["bad"]}""")) { + assertTrue( + "must not be accepted: " + body, + WavelogResponse.verdict(200, body) is WavelogResponse.Verdict.Rejected + ) + } + } + + /** + * The count check must not misfire on a real success. v1 answers with `adif_errors:0` next to + * `adif_count:1`, and matching the wrong key would reject every stored QSO. + */ + @Test + fun `adif_errors zero does not look like nothing imported`() { + for (body in listOf( + """{"status":"created","adif_count":1,"adif_errors":0,"messages":[""]}""", + """{"status":"created","adif_errors":0}""", + """{"imported":2,"skipped":1}""" + )) { + assertTrue( + "must still be accepted: " + body, + WavelogResponse.verdict(200, body) is WavelogResponse.Verdict.Accepted + ) + } + } + /** A failure with no explanation still has to say something usable. */ @Test fun `a failure without a reason still reports one`() {