SyncEngineTest.kt
264 lignes · 9099 octets
package fr.ebii.card2vcf.sync import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider import fr.ebii.card2vcf.data.CrmContactEntity import fr.ebii.card2vcf.data.CrmDatabase import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [31]) class SyncEngineTest { private lateinit var db: CrmDatabase private lateinit var api: FakeAilianceApi private lateinit var engine: SyncEngine @Before fun setUp() { val ctx = ApplicationProvider.getApplicationContext<Context>() db = Room.inMemoryDatabaseBuilder(ctx, CrmDatabase::class.java) .allowMainThreadQueries() .build() api = FakeAilianceApi() engine = SyncEngine(api, db) } @After fun tearDown() = db.close() @Test fun checkStatus_returnsTotalFromFake() = runBlocking { api.statusResult = AilianceApiClient.ApiResult.Ok( SyncStatusResponse( serverTime = "2026-07-22T12:00:00Z", changes = SyncChanges(contacts = 2, tombstones = 1), total = 3, ), ) val result = engine.checkStatus() assertEquals(3, result.pendingRemoteChanges) assertEquals(3, result.response?.total) } @Test fun syncNow_pullAppliesNewerContact() = runBlocking { db.crmContactDao().insert( CrmContactEntity( serverId = "c1", fullName = "Old Name", updatedAt = 1_000L, ), ) api.pullResult = AilianceApiClient.ApiResult.Ok( SyncPullResponse( serverTime = "2026-07-22T12:05:00Z", contacts = listOf( ContactDto( id = "c1", prenom = "Jean", nom = "Dupont", creeLe = "2026-07-20T10:00:00Z", misAJourLe = "2026-07-22T11:00:00Z", ), ), ), ) engine.syncNow() val updated = db.crmContactDao().getByServerId("c1") assertEquals("Jean Dupont", updated?.fullName) } @Test fun syncNow_pullKeepsBothAppendOnlyInteractions() = runBlocking { api.pullResult = AilianceApiClient.ApiResult.Ok( SyncPullResponse( serverTime = "2026-07-22T12:05:00Z", interactions = listOf( InteractionDto(id = "i1", contactId = "c1", sujet = "Appel", creeLe = "2026-07-22T09:00:00Z"), InteractionDto(id = "i2", contactId = "c1", sujet = "Relance", creeLe = "2026-07-22T10:00:00Z"), ), ), ) engine.syncNow() val stored = db.interactionDao().listByContactServerId("c1") assertEquals(2, stored.size) assertTrue(stored.any { it.serverId == "i1" }) assertTrue(stored.any { it.serverId == "i2" }) } @Test fun syncNow_tombstoneDeletesContact() = runBlocking { db.crmContactDao().insert( CrmContactEntity(serverId = "c-old", fullName = "À supprimer", updatedAt = 500L), ) api.pullResult = AilianceApiClient.ApiResult.Ok( SyncPullResponse( serverTime = "2026-07-22T12:05:00Z", tombstones = listOf( TombstoneDto(entityType = "contact", id = "c-old", supprimeLe = "2026-07-22T12:00:00Z"), ), ), ) engine.syncNow() assertNull(db.crmContactDao().getByServerId("c-old")) } @Test fun syncNow_updatesWatermarkFromServerTime() = runBlocking { api.pullResult = AilianceApiClient.ApiResult.Ok( SyncPullResponse(serverTime = "2026-07-22T12:05:00Z"), ) val result = engine.syncNow() assertTrue(result.success) assertEquals("2026-07-22T12:05:00Z", result.serverTime) assertEquals("2026-07-22T12:05:00Z", db.syncMetaDao().get("watermark")?.value) } @Test fun syncNow_pushesContactCreateOpAndMapsServerId() = runBlocking { val localId = db.crmContactDao().insert(CrmContactEntity(fullName = "Nouveau")) db.syncOpDao().insert( SyncOpEntity( entityType = "contact", op = "create", payloadJson = """{"fullName":"Nouveau"}""", localId = localId, createdAt = 1L, ), ) engine.syncNow() assertEquals(1, api.createContactCalls.size) assertEquals(0, db.syncOpDao().listAll().size) assertEquals("srv-generated", db.crmContactDao().getById(localId)?.serverId) } @Test fun syncNow_pushesCardImageAfterContactCreate() = runBlocking { val ctx = ApplicationProvider.getApplicationContext<Context>() val images = fr.ebii.card2vcf.data.ContactImageStore(ctx) engine = SyncEngine(api, db, images) val localId = db.crmContactDao().insert(CrmContactEntity(fullName = "Avec carte")) val cardPath = images.saveBytes(localId, "card.jpg", byteArrayOf(1, 2, 3, 4)) db.crmContactDao().update( db.crmContactDao().getById(localId)!!.copy(cardImagePath = cardPath), ) db.syncOpDao().insert( SyncOpEntity( entityType = "contact", op = "create", payloadJson = """{"prenom":"A","nom":"B"}""", localId = localId, createdAt = 1L, ), ) engine.syncNow() assertEquals(1, api.createContactCalls.size) assertEquals(1, api.uploadCarteCalls.size) assertEquals("srv-generated", api.uploadCarteCalls[0].first) assertEquals(4, api.uploadCarteCalls[0].second) assertEquals(0, db.syncOpDao().listAll().size) } @Test fun syncNow_enqueuesMediaRetryWhenUploadFails() = runBlocking { val ctx = ApplicationProvider.getApplicationContext<Context>() val images = fr.ebii.card2vcf.data.ContactImageStore(ctx) engine = SyncEngine(api, db, images) api.uploadCarteResult = AilianceApiClient.ApiResult.Err(500, "boom") val localId = db.crmContactDao().insert(CrmContactEntity(fullName = "Retry")) val cardPath = images.saveBytes(localId, "card.jpg", byteArrayOf(9)) db.crmContactDao().update( db.crmContactDao().getById(localId)!!.copy(cardImagePath = cardPath), ) db.syncOpDao().insert( SyncOpEntity( entityType = "contact", op = "create", payloadJson = """{"prenom":"A","nom":"B"}""", localId = localId, createdAt = 1L, ), ) engine.syncNow() val pending = db.syncOpDao().listAll() assertEquals(1, pending.size) assertEquals("contact_media", pending[0].entityType) assertEquals("srv-generated", pending[0].serverId) } @Test fun syncNow_pullDownloadsMissingCardImage() = runBlocking { val ctx = ApplicationProvider.getApplicationContext<Context>() val images = fr.ebii.card2vcf.data.ContactImageStore(ctx) engine = SyncEngine(api, db, images) api.downloadCarteResult = AilianceApiClient.ApiResult.Ok(byteArrayOf(7, 8, 9)) api.pullResult = AilianceApiClient.ApiResult.Ok( SyncPullResponse( serverTime = "2026-07-22T12:05:00Z", contacts = listOf( ContactDto( id = "c-img", prenom = "Marie", nom = "Curie", creeLe = "2026-07-20T10:00:00Z", carteVisite = "carte.jpg", ), ), ), ) engine.syncNow() assertEquals(listOf("c-img"), api.downloadCarteCalls) val stored = db.crmContactDao().getByServerId("c-img") assertTrue(!stored?.cardImagePath.isNullOrBlank()) assertTrue(java.io.File(stored!!.cardImagePath!!).isFile) } @Test fun syncNow_skipsImageUploadWhenNoLocalFile() = runBlocking { val localId = db.crmContactDao().insert(CrmContactEntity(fullName = "Sans image")) db.syncOpDao().insert( SyncOpEntity( entityType = "contact", op = "create", payloadJson = """{"prenom":"A","nom":"B"}""", localId = localId, createdAt = 1L, ), ) engine.syncNow() assertEquals(0, api.uploadCarteCalls.size) assertEquals(0, api.uploadPhotoCalls.size) } }
GitRust