SuppressionSyncTest.swift
179 lignes · 7075 octets
import XCTest @testable import Card2vcf /// Tests TDD pour la suppression d'interactions et de notes de projet (Lot 3). /// /// PREUVE DE ROUGEUR #1 — testPushInteraction_deleteAppelleApi /// En conservant le code d'origine `if op.op != "create" { return PushOutcome(ok: true) }` /// dans `pushInteractionOp`, le test échoue avec : /// /// XCTAssertEqual failed: ("0") is not equal to ("1") /// /// PREUVE DE ROUGEUR #2 — testPushNoteProjet_deleteAppelleApi /// En conservant `if op.op != "create" { return PushOutcome(ok: true) }` /// dans `pushNoteProjetOp`, le test échoue avec : /// /// XCTAssertEqual failed: ("0") is not equal to ("1") final class SuppressionSyncTest: 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: - Push interaction delete (PREUVE DE ROUGEUR #1) func testPushInteraction_deleteAppelleApi() async throws { var op = SyncOpEntity(entityType: "interaction", op: "delete") op.serverId = "int-srv-1" op.createdAt = 1 _ = try await db.syncOpDao.insert(op) _ = try await engine.pousserEnAttente() // Rouge sans le case "delete" dans pushInteractionOp : ("0") is not equal to ("1") XCTAssertEqual(1, api.deleteInteractionCalls.count) XCTAssertEqual("int-srv-1", api.deleteInteractionCalls.first) let ops = try await db.syncOpDao.listAll() XCTAssertTrue(ops.isEmpty, "Op dépilée après succès") } func testPushInteraction_deleteEchecGardeOp() async throws { var op = SyncOpEntity(entityType: "interaction", op: "delete") op.serverId = "int-srv-2" op.createdAt = 1 _ = try await db.syncOpDao.insert(op) api.deleteInteractionResult = .err(code: -1, message: "Réseau indisponible") _ = try await engine.pousserEnAttente() let ops = try await db.syncOpDao.listAll() XCTAssertEqual(1, ops.count, "Op gardée en file après échec réseau") } func testPushInteraction_deleteSansServerId_echoue() async throws { var op = SyncOpEntity(entityType: "interaction", op: "delete") op.serverId = nil op.createdAt = 1 _ = try await db.syncOpDao.insert(op) _ = try await engine.pousserEnAttente() // serverId nil → failure sans appel API XCTAssertEqual(0, api.deleteInteractionCalls.count) } // MARK: - Push note projet delete (PREUVE DE ROUGEUR #2) func testPushNoteProjet_deleteAppelleApi() async throws { let payload = #"{"projetServerId":"proj-srv-1"}"# var op = SyncOpEntity(entityType: "note_projet", op: "delete") op.serverId = "note-srv-1" op.payloadJson = payload op.createdAt = 1 _ = try await db.syncOpDao.insert(op) _ = try await engine.pousserEnAttente() // Rouge sans le case "delete" dans pushNoteProjetOp : ("0") is not equal to ("1") XCTAssertEqual(1, api.deleteNoteProjetCalls.count) XCTAssertEqual("proj-srv-1", api.deleteNoteProjetCalls.first?.0) XCTAssertEqual("note-srv-1", api.deleteNoteProjetCalls.first?.1) let ops = try await db.syncOpDao.listAll() XCTAssertTrue(ops.isEmpty, "Op dépilée après succès") } func testPushNoteProjet_deleteSansProjetServerId_echoue() async throws { var op = SyncOpEntity(entityType: "note_projet", op: "delete") op.serverId = "note-srv-2" op.payloadJson = "{}" // projetServerId absent op.createdAt = 1 _ = try await db.syncOpDao.insert(op) _ = try await engine.pousserEnAttente() XCTAssertEqual(0, api.deleteNoteProjetCalls.count) } // MARK: - Suppression locale sans serverId (jamais synchronisé) func testSupprimerInteractionLocale_sansServerId_neCreesPasOp() async throws { // Interaction locale uniquement (serverId nil) var entity = InteractionEntity() entity.contactServerId = "c1" entity.type = "note_vocale" entity.sujet = "Note locale" entity.createdAt = 1_000 let localId = try await db.interactionDao.upsert(entity) // Récupération et suppression locale guard let found = try await db.interactionDao.getByLocalId(localId) else { XCTFail("Entité introuvable") return } XCTAssertNil(found.serverId, "serverId doit être nil pour une entité jamais sync") // Suppression sans op de sync (serverId absent) try await db.interactionDao.deleteByLocalId(localId) let allInteractions = try await db.interactionDao.listAll() XCTAssertTrue(allInteractions.isEmpty, "Interaction supprimée localement") let allOps = try await db.syncOpDao.listAll() XCTAssertTrue(allOps.isEmpty, "Aucun op créé pour une entité jamais sync") } func testSupprimerNoteLocale_sansServerId_neCreesPasOp() async throws { var entity = NoteProjetEntity() entity.projetServerId = "proj-1" entity.titre = "Note locale" entity.createdAt = 1_000 let localId = try await db.noteProjetDao.upsert(entity) let noteInserted = try await db.noteProjetDao.listAll().first { $0.localId == localId } XCTAssertNil(noteInserted?.serverId) try await db.noteProjetDao.deleteByLocalId(localId) let allNotes = try await db.noteProjetDao.listAll() XCTAssertTrue(allNotes.isEmpty) let allOps = try await db.syncOpDao.listAll() XCTAssertTrue(allOps.isEmpty) } // MARK: - Suppression synchronisée crée l'op func testSupprimerInteractionSynchronisee_creeOp() async throws { var entity = InteractionEntity() entity.serverId = "int-srv-ok" entity.contactServerId = "c1" entity.type = "note_vocale" entity.sujet = "Note sync" entity.createdAt = 1_000 let localId = try await db.interactionDao.upsert(entity) // Simulation : supprimer localement + créer l'op de sync guard let found = try await db.interactionDao.getByLocalId(localId), let serverId = found.serverId else { XCTFail("Entité introuvable") return } try await db.interactionDao.deleteByLocalId(localId) var op = SyncOpEntity(entityType: "interaction", op: "delete") op.serverId = serverId op.createdAt = 2_000 _ = try await db.syncOpDao.insert(op) let allInteractions = try await db.interactionDao.listAll() XCTAssertTrue(allInteractions.isEmpty, "Interaction supprimée localement") let allOps = try await db.syncOpDao.listAll() XCTAssertEqual(1, allOps.count, "Op de delete créée") XCTAssertEqual("delete", allOps.first?.op) XCTAssertEqual("int-srv-ok", allOps.first?.serverId) } }
GitRust