AilianceApiClientTest.kt 361 lignes · 13030 octets
package fr.ebii.card2vcf.sync

import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class AilianceApiClientTest {
    private lateinit var server: MockWebServer
    private lateinit var client: AilianceApiClient

    @Before
    fun setUp() {
        server = MockWebServer()
        server.start()
        client = AilianceApiClient(
            baseUrl = server.url("/").toString().trimEnd('/'),
            apiKey = "test-bearer-key",
        )
    }

    @After
    fun tearDown() {
        server.shutdown()
    }

    @Test
    fun authCle200ParsesResponse() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody(
                    """
                    {
                      "nom": "alice",
                      "cle_chiffree": "Y2lw",
                      "sel": "c2Vs",
                      "kdf": "argon2id",
                      "cipher": "xchacha20poly1305"
                    }
                    """.trimIndent(),
                ),
        )

        val result = client.authCle("alice", "secret")

        assertTrue(result is AilianceApiClient.ApiResult.Ok)
        val ok = result as AilianceApiClient.ApiResult.Ok
        assertEquals("alice", ok.value.nom)
        assertEquals("Y2lw", ok.value.cleChiffree)
        assertEquals("c2Vs", ok.value.sel)
        assertEquals("argon2id", ok.value.kdf)
        assertEquals("xchacha20poly1305", ok.value.cipher)

        val request = server.takeRequest()
        assertEquals("POST", request.method)
        assertEquals("/api/auth/cle", request.path)
        assertTrue(request.getHeader("Authorization") == null)
    }

    @Test
    fun authCle401ReturnsErr() {
        server.enqueue(
            MockResponse()
                .setResponseCode(401)
                .setBody("""{"message":"Identifiants invalides."}"""),
        )

        val result = client.authCle("alice", "wrong")

        assertTrue(result is AilianceApiClient.ApiResult.Err)
        val err = result as AilianceApiClient.ApiResult.Err
        assertEquals(401, err.code)
        assertEquals("Identifiants invalides.", err.message)
    }

    @Test
    fun syncStatus200ParsesTotalAndChanges() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody(
                    """
                    {
                      "server_time": "2026-07-22T12:00:00Z",
                      "changes": {
                        "contacts": 2,
                        "entreprises": 0,
                        "projets": 1,
                        "taches": 3,
                        "interactions": 1,
                        "tombstones": 1
                      },
                      "total": 8
                    }
                    """.trimIndent(),
                ),
        )

        val result = client.syncStatus("1970-01-01T00:00:00Z")

        assertTrue(result is AilianceApiClient.ApiResult.Ok)
        val ok = result as AilianceApiClient.ApiResult.Ok
        assertEquals("2026-07-22T12:00:00Z", ok.value.serverTime)
        assertEquals(8, ok.value.total)
        assertEquals(2, ok.value.changes.contacts)
        assertEquals(1, ok.value.changes.tombstones)

        val request = server.takeRequest()
        assertEquals("GET", request.method)
        assertEquals("/api/sync/status?since=1970-01-01T00%3A00%3A00Z", request.path)
        assertEquals("Bearer test-bearer-key", request.getHeader("Authorization"))
    }

    @Test
    fun syncPull200ParsesContactsAndTombstones() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody(
                    """
                    {
                      "server_time": "2026-07-22T12:05:00Z",
                      "contacts": [
                        {
                          "id": "c1",
                          "prenom": "Jean",
                          "nom": "Dupont",
                          "cree_le": "2026-07-20T10:00:00Z",
                          "mis_a_jour_le": "2026-07-21T08:00:00Z"
                        }
                      ],
                      "entreprises": [],
                      "projets": [],
                      "taches": [
                        {
                          "projet_id": "p1",
                          "id": "t1",
                          "titre": "Appeler client",
                          "colonne_id": "col-a-faire",
                          "ordre": 0,
                          "cree_le": "2026-07-21T09:00:00Z",
                          "assigne_a": "alice"
                        }
                      ],
                      "interactions": [],
                      "tombstones": [
                        {
                          "type": "contact",
                          "id": "c-old",
                          "supprime_le": "2026-07-22T11:00:00Z"
                        }
                      ],
                      "workflows": []
                    }
                    """.trimIndent(),
                ),
        )

        val result = client.syncPull("2026-07-20T00:00:00Z")

        assertTrue(result is AilianceApiClient.ApiResult.Ok)
        val ok = result as AilianceApiClient.ApiResult.Ok
        assertEquals(1, ok.value.contacts.size)
        assertEquals("c1", ok.value.contacts[0].id)
        assertEquals("Jean", ok.value.contacts[0].prenom)
        assertEquals(1, ok.value.taches.size)
        assertEquals("p1", ok.value.taches[0].projetId)
        assertEquals("alice", ok.value.taches[0].assigneA)
        assertEquals(1, ok.value.tombstones.size)
        assertEquals("contact", ok.value.tombstones[0].entityType)
        assertEquals("c-old", ok.value.tombstones[0].id)
    }

    @Test
    fun syncPull200ParsesAgendaFields() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody(
                    """
                    {
                      "server_time": "2026-07-22T12:05:00Z",
                      "contacts": [],
                      "entreprises": [],
                      "projets": [],
                      "taches": [],
                      "interactions": [],
                      "rdv": [
                        {
                          "id": "rdv1",
                          "titre": "Point client",
                          "utilisateur": "alice",
                          "debut": "2026-07-22T09:00:00Z",
                          "fin": "2026-07-22T10:00:00Z",
                          "cree_par": "alice",
                          "cree_le": "2026-07-20T08:00:00Z"
                        }
                      ],
                      "reservations": [
                        {
                          "id": "resa1",
                          "cible": {"type": "salle", "id": "salle-a"},
                          "debut": "2026-07-22T09:00:00Z",
                          "fin": "2026-07-22T10:00:00Z",
                          "motif": "Réunion",
                          "statut": "active",
                          "reserve_par": "alice",
                          "cree_le": "2026-07-20T08:00:00Z"
                        }
                      ],
                      "indisponibilites": [
                        {
                          "id": "indispo1",
                          "cible": {"type": "vehicule", "id": "vehicule-a"},
                          "nature": "entretien",
                          "debut": "2026-07-22T09:00:00Z",
                          "fin": "2026-07-22T10:00:00Z",
                          "cree_par": "alice",
                          "cree_le": "2026-07-20T08:00:00Z"
                        }
                      ],
                      "tombstones": [],
                      "workflows": []
                    }
                    """.trimIndent(),
                ),
        )

        val result = client.syncPull("2026-07-20T00:00:00Z")

        assertTrue(result is AilianceApiClient.ApiResult.Ok)
        val ok = result as AilianceApiClient.ApiResult.Ok
        assertEquals(1, ok.value.rdv.size)
        assertEquals("Point client", ok.value.rdv[0].titre)
        assertEquals(1, ok.value.reservations.size)
        assertEquals("salle", ok.value.reservations[0].cible.type)
        assertEquals("salle-a", ok.value.reservations[0].cible.id)
        assertEquals(1, ok.value.indisponibilites.size)
        assertEquals("entretien", ok.value.indisponibilites[0].nature)
    }

    @Test
    fun createReservation409ReturnsErrWithCode() {
        server.enqueue(
            MockResponse()
                .setResponseCode(409)
                .setBody("""{"message":"Réservation refusée : période en conflit.","reclamable":true}"""),
        )

        val result = client.createReservation(
            "salle",
            "salle-a",
            """{"debut":"2026-07-22T09:00:00Z","fin":"2026-07-22T10:00:00Z","motif":"Réunion"}""",
        )

        assertTrue(result is AilianceApiClient.ApiResult.Err)
        val err = result as AilianceApiClient.ApiResult.Err
        assertEquals(409, err.code)
        assertEquals("Réservation refusée : période en conflit.", err.message)

        val request = server.takeRequest()
        assertEquals("POST", request.method)
        assertEquals("/api/ressources/salle/salle-a/reservations", request.path)
    }

    @Test
    fun getRessourcesCatalogue200ParsesCatalogue() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody(
                    """
                    {
                      "salles": [{"id": "salle-a", "nom": "Salle A", "actif": true}],
                      "materiels": [],
                      "vehicules": [{"id": "vehicule-a", "nom": "Véhicule A", "actif": false}]
                    }
                    """.trimIndent(),
                ),
        )

        val result = client.getRessourcesCatalogue()

        assertTrue(result is AilianceApiClient.ApiResult.Ok)
        val ok = result as AilianceApiClient.ApiResult.Ok
        assertEquals(1, ok.value.salles.size)
        assertEquals("Salle A", ok.value.salles[0].nom)
        assertTrue(ok.value.salles[0].actif)
        assertEquals(0, ok.value.materiels.size)
        assertEquals(1, ok.value.vehicules.size)
        assertTrue(!ok.value.vehicules[0].actif)

        val request = server.takeRequest()
        assertEquals("GET", request.method)
        assertEquals("/api/ressources", request.path)
    }

    @Test
    fun protectedEndpointWithoutBearerReturns401() {
        val unauthenticated = AilianceApiClient(
            baseUrl = server.url("/").toString().trimEnd('/'),
            apiKey = null,
        )
        server.enqueue(
            MockResponse()
                .setResponseCode(401)
                .setBody("""{"message":"Non autorisé."}"""),
        )

        val result = unauthenticated.syncStatus("1970-01-01T00:00:00Z")

        assertTrue(result is AilianceApiClient.ApiResult.Err)
        val err = result as AilianceApiClient.ApiResult.Err
        assertEquals(401, err.code)

        val request = server.takeRequest()
        assertTrue(request.getHeader("Authorization") == null)
    }

    @Test
    fun uploadContactCarte_sendsMultipartWithBearer() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody("""{"id":"c1","carte_visite":"carte.jpg","cree_le":"2026-07-22T00:00:00Z"}"""),
        )

        val result = client.uploadContactCarte("c1", byteArrayOf(1, 2, 3), "card.jpg")

        assertTrue(result is AilianceApiClient.ApiResult.Ok)
        val request = server.takeRequest()
        assertEquals("POST", request.method)
        assertEquals("/api/contacts/c1/carte", request.path)
        assertEquals("Bearer test-bearer-key", request.getHeader("Authorization"))
        val body = request.body.readUtf8()
        assertTrue(body.contains("filename=\"card.jpg\""))
        assertTrue(request.getHeader("Content-Type")!!.startsWith("multipart/form-data"))
    }

    @Test
    fun downloadContactCarte_returnsBytes() {
        server.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setHeader("Content-Type", "image/jpeg")
                .setBody(okio.Buffer().write(byteArrayOf(10, 20, 30))),
        )

        val result = client.downloadContactCarte("c1")

        assertTrue(result is AilianceApiClient.ApiResult.Ok)
        assertTrue((result as AilianceApiClient.ApiResult.Ok).value.contentEquals(byteArrayOf(10, 20, 30)))
        val request = server.takeRequest()
        assertEquals("GET", request.method)
        assertEquals("/api/contacts/c1/carte", request.path)
    }
}