NoteProjetSyncEngineTest.swift
186 lignes · 7411 octets
import XCTest @testable import Card2vcf /// Preuve de rougeur (exigence du plan `docs/plans/rattrapage-ios-et-tests.md`, § 2A) : /// en neutralisant la garde LWW de `SyncEngine.applyNoteProjet` /// (`guard remoteTs > localTs else { return }`), la suite passe de 204/0 à 204/3 échecs : /// testPullNoteUpdatePlusRecentApplique — XCTAssertEqual failed: /// ("Optional("Nouveau titre")") is not equal to ("Optional("Ancien titre")") /// testPullNoteUpdatePreserveAudioPath — XCTAssertEqual failed: /// ("Optional("Note vocale éditée")") is not equal to ("Optional("Note vocale")") /// Ces tests détectent donc réellement la perte de la mise à jour LWW. final class NoteProjetSyncEngineTest: XCTestCase { private var db: InMemoryCrmDatabase! private var api: FakeAilianceApi! private var engine: SyncEngine! override func setUp() { super.setUp() db = InMemoryCrmDatabase() api = FakeAilianceApi() engine = SyncEngine(api: api, db: db) } // MARK: - Insert func testPullNoteInsertNouvelle() async throws { api.pullResult = .ok(SyncPullResponse( serverTime: "2026-09-12T09:00:00Z", notes: [ NoteProjetDto( id: "n1", projetId: "p1", titre: "Note initiale", contenu: "## Contenu", creeLe: "2026-09-10T08:00:00Z" ) ] )) _ = try await engine.syncNow() let stored = try await db.noteProjetDao.getByServerId("n1") XCTAssertNotNil(stored, "La note doit \u{ea}tre ins\u{e9}r\u{e9}e lors du pull") XCTAssertEqual("Note initiale", stored?.titre) XCTAssertEqual("## Contenu", stored?.texte) XCTAssertEqual("p1", stored?.projetServerId) } // MARK: - LWW update func testPullNoteUpdatePlusRecentApplique() async throws { // Note locale \u{e0} updatedAt = 1000 ms var local = NoteProjetEntity() local.serverId = "n2"; local.projetServerId = "p1" local.titre = "Ancien titre"; local.texte = "Ancien texte"; local.auteur = "alice" local.createdAt = 1_000; local.updatedAt = 1_000 try await db.noteProjetDao.upsert(local) // Remote maj_le 2026-09-12 >> local 1000 ms api.pullResult = .ok(SyncPullResponse( serverTime: "2026-09-12T10:00:00Z", notes: [ NoteProjetDto( id: "n2", projetId: "p1", titre: "Nouveau titre", contenu: "Nouveau texte", auteur: "alice", creeLe: "2026-09-10T08:00:00Z", majLe: "2026-09-12T09:00:00Z" ) ] )) _ = try await engine.syncNow() let stored = try await db.noteProjetDao.getByServerId("n2") XCTAssertEqual("Nouveau titre", stored?.titre, "La mise \u{e0} jour plus r\u{e9}cente doit \u{ea}tre appliqu\u{e9}e") XCTAssertEqual("Nouveau texte", stored?.texte) } func testPullNoteUpdatePlusAncienIgnore() async throws { // Note locale \u{e0} updatedAt tr\u{e8}s r\u{e9}cent (apr\u{e8}s 2030) var local = NoteProjetEntity() local.serverId = "n3"; local.projetServerId = "p1" local.titre = "Titre local"; local.texte = "Texte local"; local.auteur = "bob" local.createdAt = 2_000_000_000_000 // ~2033 en ms local.updatedAt = 2_000_000_000_000 try await db.noteProjetDao.upsert(local) // Remote cree_le 2026 << local 2033 api.pullResult = .ok(SyncPullResponse( serverTime: "2026-09-12T10:00:00Z", notes: [ NoteProjetDto( id: "n3", projetId: "p1", titre: "Titre serveur", contenu: "Texte serveur", creeLe: "2026-09-10T08:00:00Z" ) ] )) _ = try await engine.syncNow() let stored = try await db.noteProjetDao.getByServerId("n3") XCTAssertEqual("Titre local", stored?.titre, "La mise \u{e0} jour plus ancienne NE doit PAS \u{ea}tre appliqu\u{e9}e") } // MARK: - audioPath pr\u{e9}serv\u{e9} func testPullNoteUpdatePreserveAudioPath() async throws { var local = NoteProjetEntity() local.serverId = "n4"; local.projetServerId = "p1" local.titre = "Note vocale"; local.texte = "Texte initial"; local.auteur = "alice" local.audioPath = "/local/notes/n4.wav" local.createdAt = 1_000; local.updatedAt = 1_000 try await db.noteProjetDao.upsert(local) // Remote plus r\u{e9}cent, avec champ audio serveur (nom de fichier, pas chemin local) api.pullResult = .ok(SyncPullResponse( serverTime: "2026-09-12T10:00:00Z", notes: [ NoteProjetDto( id: "n4", projetId: "p1", titre: "Note vocale \u{e9}dit\u{e9}e", contenu: "Texte \u{e9}dit\u{e9}", creeLe: "2026-09-10T08:00:00Z", majLe: "2026-09-12T09:00:00Z", audio: "n4.wav" ) ] )) _ = try await engine.syncNow() let stored = try await db.noteProjetDao.getByServerId("n4") XCTAssertEqual("Note vocale \u{e9}dit\u{e9}e", stored?.titre) XCTAssertEqual("/local/notes/n4.wav", stored?.audioPath, "audioPath local doit \u{ea}tre pr\u{e9}serv\u{e9} lors d'une mise \u{e0} jour serveur") } // MARK: - Tombstone func testPullTombstoneNoteSupprimeNote() async throws { var local = NoteProjetEntity() local.serverId = "n-del"; local.projetServerId = "p1" local.titre = "\u{e0} supprimer"; local.texte = ""; local.auteur = ""; local.createdAt = 1 try await db.noteProjetDao.upsert(local) api.pullResult = .ok(SyncPullResponse( serverTime: "2026-09-12T10:00:00Z", tombstones: [ TombstoneDto(entityType: "note", id: "n-del", supprimeLe: "2026-09-12T10:00:00Z") ] )) _ = try await engine.syncNow() let stored = try await db.noteProjetDao.getByServerId("n-del") XCTAssertNil(stored, "La note supprim\u{e9}e par tombstone doit disparaitre") } // MARK: - R\u{e9}ponse sans section notes func testPullSansNotesNePasPlanter() async throws { // Serveur antérieur : aucun champ "notes" dans la réponse api.pullResult = .ok(SyncPullResponse(serverTime: "2026-09-12T10:00:00Z")) let result = try await engine.syncNow() XCTAssertTrue(result.success) let notes = try await db.noteProjetDao.listAll() XCTAssertTrue(notes.isEmpty) } // MARK: - Comptage dans received func testPullNotesCompteesDansReceived() async throws { api.pullResult = .ok(SyncPullResponse( serverTime: "2026-09-12T10:00:00Z", notes: [ NoteProjetDto(id: "n1", projetId: "p1", titre: "A", contenu: "a", creeLe: "2026-09-10T08:00:00Z"), NoteProjetDto(id: "n2", projetId: "p1", titre: "B", contenu: "b", creeLe: "2026-09-10T08:00:00Z"), ] )) let result = try await engine.syncNow() XCTAssertEqual(2, result.received, "Les notes doivent \u{ea}tre compt\u{e9}es dans received") } }
GitRust