bitchat/localPackages/BitFoundation/Tests/BitFoundationTests/DeliveryStatusNotSentYetTests.swift
Vidit Kulshrestha 81837d7202
Make DeliveryStatus non-optional with an explicit .notSentYet state (#1503)
BitchatMessage.deliveryStatus was Optional, with nil implicitly meaning
'no tracking' for public messages. Every consumer had to branch on the
absent case, ranking needed an optional-aware helper, and the UI treated
nil as an invisible state (#644).

Model delivery as a total state machine instead:
- New DeliveryStatus.notSentYet: created but not yet handed to any
  transport. Public messages initialize to it; private messages keep
  their historical .sending default.
- BitchatMessage.deliveryStatus becomes non-optional. Archives written
  while the field was optional decode with the absent key mapped to
  .notSentYet. The wire format is untouched (toBinaryPayload never
  carried the field).
- deliveryStatusRank drops its optional parameter; .notSentYet ranks
  below .failed, preserving the existing dedup preference order.
- Conversation.shouldSkipStatusUpdate treats a write back to .notSentYet
  as a downgrade and skips it.
- The status indicator renders exactly as before: .notSentYet draws
  nothing in message rows (the state nil used to represent), and
  DeliveryStatusView gains a glyph and description for it only so the
  view stays total.

Tests: initialization defaults, legacy-archive decoding, round-trip,
the extended rank order, and the new downgrade rule.

Fixes #644
2026-07-30 17:56:42 +01:00

60 lines
2.4 KiB
Swift

//
// DeliveryStatusNotSentYetTests.swift
// bitchatTests
//
// DeliveryStatus is a total state machine: every message carries a concrete
// status from creation. Public messages start .notSentYet, private messages
// keep their historical .sending default, and archives persisted while the
// field was optional decode with the absent field mapped to .notSentYet.
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Testing
import Foundation
@testable import BitFoundation
struct DeliveryStatusNotSentYetTests {
private func makeMessage(isPrivate: Bool, deliveryStatus: DeliveryStatus? = nil) -> BitchatMessage {
BitchatMessage(
sender: "alice",
content: "hello",
timestamp: Date(timeIntervalSince1970: 1_000),
isRelay: false,
isPrivate: isPrivate,
deliveryStatus: deliveryStatus
)
}
@Test
func publicMessagesStartNotSentYetAndPrivateStartSending() {
#expect(makeMessage(isPrivate: false).deliveryStatus == .notSentYet)
#expect(makeMessage(isPrivate: true).deliveryStatus == .sending)
// An explicit status always wins over the defaults.
#expect(makeMessage(isPrivate: false, deliveryStatus: .sent).deliveryStatus == .sent)
}
@Test
func decodingLegacyArchiveWithoutStatusYieldsNotSentYet() throws {
// Pre-existing archives omitted the key for public messages while the
// field was optional; absent must map to .notSentYet, not fail.
let encoded = try JSONEncoder().encode(makeMessage(isPrivate: false))
var json = try #require(
JSONSerialization.jsonObject(with: encoded) as? [String: Any]
)
json.removeValue(forKey: "deliveryStatus")
let legacyData = try JSONSerialization.data(withJSONObject: json)
let decoded = try JSONDecoder().decode(BitchatMessage.self, from: legacyData)
#expect(decoded.deliveryStatus == .notSentYet)
}
@Test
func decodingRoundTripPreservesConcreteStatus() throws {
let message = makeMessage(isPrivate: true, deliveryStatus: .delivered(to: "bob", at: Date(timeIntervalSince1970: 2_000)))
let decoded = try JSONDecoder().decode(BitchatMessage.self, from: JSONEncoder().encode(message))
#expect(decoded.deliveryStatus == .delivered(to: "bob", at: Date(timeIntervalSince1970: 2_000)))
}
}