crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

MovesCodecLegacyDecodeTests.swift (4398B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import Crossmate
      5 
      6 /// Pins down `MovesCodec`'s one back-compat concession: decoding `Moves` blobs
      7 /// written before the single-`markCode` cutover, which encoded the mark as a
      8 /// `(markKind, checkedRight, checkedWrong)` triple. Records at rest in CloudKit
      9 /// (and any straggler peer) still carry that shape, so the fallback must lift
     10 /// it to the right `CellMark`. New writes are markCode-only — also asserted
     11 /// here so the legacy keys never creep back into the wire format.
     12 @Suite("MovesCodec legacy decode")
     13 struct MovesCodecLegacyDecodeTests {
     14 
     15     /// Builds an old-format blob: one entry per (markKind, checkedRight,
     16     /// checkedWrong) triple, in the JSON shape a pre-cutover client produced.
     17     /// `updatedAt` is a bare number because `MovesCodec` uses a plain
     18     /// `JSONEncoder`/`Decoder` (deferred-to-date strategy = seconds).
     19     private func legacyBlob(_ triples: [(row: Int, kind: Int, right: Bool, wrong: Bool)]) throws -> Data {
     20         let entries: [[String: Any]] = triples.map { t in
     21             [
     22                 "row": t.row, "col": 0, "letter": "A",
     23                 "markKind": t.kind,
     24                 "checkedRight": t.right,
     25                 "checkedWrong": t.wrong,
     26                 "updatedAt": 0.0,
     27                 "authorID": "alice",
     28             ]
     29         }
     30         return try JSONSerialization.data(withJSONObject: ["entries": entries])
     31     }
     32 
     33     @Test("legacy triple decodes to the right CellMark for every kind/check")
     34     func legacyTripleDecodes() throws {
     35         let blob = try legacyBlob([
     36             (row: 0, kind: 0, right: false, wrong: false), // .none
     37             (row: 1, kind: 1, right: false, wrong: false), // .pen(nil)
     38             (row: 2, kind: 1, right: true, wrong: false),  // .pen(.right)
     39             (row: 3, kind: 1, right: false, wrong: true),  // .pen(.wrong)
     40             (row: 4, kind: 2, right: false, wrong: false), // .pencil(nil)
     41             (row: 5, kind: 2, right: true, wrong: false),  // .pencil(.right)
     42             (row: 6, kind: 2, right: false, wrong: true),  // .pencil(.wrong)
     43             (row: 7, kind: 3, right: false, wrong: false), // .revealed
     44         ])
     45 
     46         let cells = try MovesCodec.decode(blob)
     47         func mark(_ row: Int) -> CellMark? { cells[GridPosition(row: row, col: 0)]?.mark }
     48 
     49         #expect(mark(0) == CellMark.none)
     50         #expect(mark(1) == .pen(checked: nil))
     51         #expect(mark(2) == .pen(checked: .right))
     52         #expect(mark(3) == .pen(checked: .wrong))
     53         #expect(mark(4) == .pencil(checked: nil))
     54         #expect(mark(5) == .pencil(checked: .right))
     55         #expect(mark(6) == .pencil(checked: .wrong))
     56         #expect(mark(7) == .revealed)
     57     }
     58 
     59     @Test("oldest blobs without a checkedRight key still decode")
     60     func missingCheckedRightKey() throws {
     61         // `checkedRight` was added after the first release; the very oldest
     62         // records omit it entirely. It must default to false, not throw.
     63         let entry: [String: Any] = [
     64             "row": 0, "col": 0, "letter": "Q",
     65             "markKind": 1, "checkedWrong": false,
     66             "updatedAt": 0.0, "authorID": "alice",
     67         ]
     68         let blob = try JSONSerialization.data(withJSONObject: ["entries": [entry]])
     69 
     70         let cells = try MovesCodec.decode(blob)
     71         #expect(cells[GridPosition(row: 0, col: 0)]?.mark == .pen(checked: nil))
     72     }
     73 
     74     @Test("new writes are markCode-only — no legacy keys on the wire")
     75     func newFormatOmitsLegacyKeys() throws {
     76         let cells: [GridPosition: TimestampedCell] = [
     77             GridPosition(row: 0, col: 0): TimestampedCell(
     78                 letter: "A", mark: .pencil(checked: .wrong),
     79                 updatedAt: Date(timeIntervalSinceReferenceDate: 0), authorID: "alice"
     80             ),
     81         ]
     82         let data = try MovesCodec.encode(cells)
     83 
     84         let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
     85         let entry = (json?["entries"] as? [[String: Any]])?.first
     86         #expect(entry?["markCode"] as? Int == 6) // .pencil(.wrong)
     87         #expect(entry?["markKind"] == nil)
     88         #expect(entry?["checkedRight"] == nil)
     89         #expect(entry?["checkedWrong"] == nil)
     90 
     91         // And it round-trips back through decode.
     92         let decoded = try MovesCodec.decode(data)
     93         #expect(decoded[GridPosition(row: 0, col: 0)]?.mark == .pencil(checked: .wrong))
     94     }
     95 }