// SPDX-License-Identifier: GPL-3.0-or-later // MyIce Kotlin Multiplatform — schedule/convocation viewer // Copyright (C) 2026 parano.ch // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package ch.parano.myice.viewmodel import ch.parano.myice.cache.ScheduleCache import ch.parano.myice.filter.TypeFilter import ch.parano.myice.models.Account import ch.parano.myice.models.Event import ch.parano.myice.network.ApiService import com.russhwolf.settings.MapSettings import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockEngineConfig import io.ktor.client.engine.mock.respond import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.headersOf import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import kotlinx.serialization.json.Json import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class ScheduleViewModelTest { @BeforeTest fun setUpDispatcher() { Dispatchers.setMain(UnconfinedTestDispatcher()) } @AfterTest fun tearDownDispatcher() { Dispatchers.resetMain() } private val json = Json { ignoreUnknownKeys = true } private fun mockApi(accountsJson: String, scheduleJson: String): ApiService { val config = MockEngineConfig().apply { addHandler { request -> val path = request.url.encodedPath val response = when { path == "/accounts" -> accountsJson path == "/schedule" -> scheduleJson else -> "[]" } respond( content = response, status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/json"), ) } dispatcher = Dispatchers.Unconfined } val client = HttpClient(MockEngine(config)) { install(ContentNegotiation) { json(json) } } return ApiService(client) } private fun sampleEvent( id: String = "1", agegroup: String = "U13 (Elite)", name: String = "U13 Elite - HC Ajoie", eventType: String = "Jeu", ) = Event( idEvent = id, agegroup = agegroup, name = name, title = "Title", opponent = "Opp", place = "Place", start = "2024-11-10T14:00:00", end = "2024-11-10T16:15:00", color = "#e4222e", eventType = eventType, ) @Test fun testLoadAccounts() = runTest { val api = mockApi( """[{"name":"default","label":"Default"}]""", "[]", ) val cache = ScheduleCache(MapSettings()) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() assertEquals(1, vm.state.value.accounts.size) assertEquals("default", vm.state.value.selectedAccount) } @Test fun testRefreshScheduleStoresEvents() = runTest { val scheduleJson = """ [{"id_event":"1","agegroup":"U13 (Elite)","name":"U13 Elite - HC Ajoie","title":"T","opponent":"O","place":"P","start":"2024-11-10T14:00:00","end":"2024-11-10T16:15:00","color":"#e4222e","event":"Jeu"}] """.trimIndent() val api = mockApi("""[{"name":"default","label":"Default"}]""", scheduleJson) val cache = ScheduleCache(MapSettings()) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() vm.refreshSchedule() assertEquals(1, vm.state.value.events.size) assertEquals("1", vm.state.value.events[0].idEvent) } @Test fun testFilteredEventsDefaultGamesOnly() = runTest { val scheduleJson = """ [{"id_event":"1","agegroup":"U13","name":"U13 - A","title":"T","opponent":"O","place":"P","start":"s","end":"e","event":"Jeu"}, {"id_event":"2","agegroup":"U13","name":"U13 - B","title":"T","opponent":"","place":"P","start":"s","end":"e"}] """.trimIndent() val api = mockApi("""[{"name":"default","label":"Default"}]""", scheduleJson) val cache = ScheduleCache(MapSettings()) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() vm.refreshSchedule() assertEquals(2, vm.state.value.events.size) assertEquals(1, vm.state.value.filteredEvents.size) assertEquals("1", vm.state.value.filteredEvents[0].idEvent) } @Test fun testSetTypeFilterAll() = runTest { val scheduleJson = """ [{"id_event":"1","agegroup":"U13","name":"U13 - A","title":"T","opponent":"O","place":"P","start":"s","end":"e","event":"Jeu"}, {"id_event":"2","agegroup":"U13","name":"U13 - B","title":"T","opponent":"","place":"P","start":"s","end":"e"}] """.trimIndent() val api = mockApi("""[{"name":"default","label":"Default"}]""", scheduleJson) val cache = ScheduleCache(MapSettings()) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() vm.refreshSchedule() vm.setTypeFilter(TypeFilter.ALL) assertEquals(2, vm.state.value.filteredEvents.size) } @Test fun testSetAgegroup() = runTest { val scheduleJson = """ [{"id_event":"1","agegroup":"U13","name":"U13 - A","title":"T","opponent":"O","place":"P","start":"s","end":"e","event":"Jeu"}, {"id_event":"2","agegroup":"U15","name":"U15 - B","title":"T","opponent":"O","place":"P","start":"s","end":"e","event":"Jeu"}] """.trimIndent() val api = mockApi("""[{"name":"default","label":"Default"}]""", scheduleJson) val cache = ScheduleCache(MapSettings()) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() vm.refreshSchedule() vm.setAgegroup("U15") assertEquals(1, vm.state.value.filteredEvents.size) assertEquals("2", vm.state.value.filteredEvents[0].idEvent) } @Test fun testSetSubgroup() = runTest { val scheduleJson = """ [{"id_event":"1","agegroup":"U13","name":"U13 Elite - HC Ajoie","title":"T","opponent":"O","place":"P","start":"s","end":"e","event":"Jeu"}, {"id_event":"2","agegroup":"U13","name":"U13 Top - HC Geneva","title":"T","opponent":"O","place":"P","start":"s","end":"e","event":"Jeu"}] """.trimIndent() val api = mockApi("""[{"name":"default","label":"Default"}]""", scheduleJson) val cache = ScheduleCache(MapSettings()) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() vm.refreshSchedule() vm.setSubgroup("U13 Elite") assertEquals(1, vm.state.value.filteredEvents.size) assertEquals("1", vm.state.value.filteredEvents[0].idEvent) } @Test fun testCacheLoadOnLoadCachedSchedule() = runTest { val api = mockApi("""[{"name":"default","label":"Default"}]""", "[]") val cache = ScheduleCache(MapSettings()) cache.saveEvents("default", listOf(sampleEvent("1"))) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() vm.loadCachedSchedule() assertEquals(1, vm.state.value.events.size) assertFalse(vm.state.value.isLoading) } @Test fun testAgegroupsComputed() = runTest { val scheduleJson = """ [{"id_event":"1","agegroup":"U15","name":"N","title":"T","opponent":"","place":"P","start":"s","end":"e","event":"Jeu"}, {"id_event":"2","agegroup":"U13","name":"N","title":"T","opponent":"","place":"P","start":"s","end":"e","event":"Jeu"}] """.trimIndent() val api = mockApi("""[{"name":"default","label":"Default"}]""", scheduleJson) val cache = ScheduleCache(MapSettings()) val vm = ScheduleViewModel(api, cache) vm.loadAccounts() vm.refreshSchedule() assertEquals(listOf("U13", "U15"), vm.state.value.agegroups) } }