ref: Rename package ch.parano.myice to ch.parano.myicek
Complete the MyIceK rename by updating the Kotlin package from ch.parano.myice to ch.parano.myicek across all source sets (androidMain, commonMain, desktopMain, iosMain, commonTest), the build.gradle.kts (namespace, applicationId, mainClass, desktop packageName) and the iOS Info.plist bundle URL name. The myice:// OAuth scheme is kept unchanged as it depends on the backend. Also add pull-to-refresh to ScheduleScreen using Material 3 PullToRefreshBox, replacing the manual loading overlay.
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package ch.parano.myicek.cache
|
||||
|
||||
import ch.parano.myicek.models.Event
|
||||
import com.russhwolf.settings.MapSettings
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ScheduleCacheTest {
|
||||
|
||||
private fun createCache(): Pair<ScheduleCache, Settings> {
|
||||
val settings = MapSettings()
|
||||
return ScheduleCache(settings) to settings
|
||||
}
|
||||
|
||||
private fun sampleEvent(id: String, eventType: String = "Jeu") = Event(
|
||||
idEvent = id,
|
||||
agegroup = "U13 (Elite)",
|
||||
name = "U13 Elite - HC Ajoie",
|
||||
title = "U13 (Elite)\nGame\nArena",
|
||||
opponent = "HC Ajoie",
|
||||
place = "Arena",
|
||||
start = "2024-11-10T14:00:00",
|
||||
end = "2024-11-10T16:15:00",
|
||||
color = "#e4222e",
|
||||
eventType = eventType,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testSaveAndLoadEvents() {
|
||||
val (cache, _) = createCache()
|
||||
val events = listOf(sampleEvent("1"), sampleEvent("2"))
|
||||
cache.saveEvents("default", events)
|
||||
|
||||
val loaded = cache.loadEvents("default")
|
||||
assertNotNull(loaded)
|
||||
assertEquals(2, loaded.size)
|
||||
assertEquals("1", loaded[0].idEvent)
|
||||
assertEquals("2", loaded[1].idEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadEventsReturnsNullWhenEmpty() {
|
||||
val (cache, _) = createCache()
|
||||
assertNull(cache.loadEvents("default"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetLastUpdated() {
|
||||
val (cache, _) = createCache()
|
||||
assertNull(cache.getLastUpdated("default"))
|
||||
|
||||
cache.saveEvents("default", listOf(sampleEvent("1")))
|
||||
val timestamp = cache.getLastUpdated("default")
|
||||
assertNotNull(timestamp)
|
||||
assertTrue(timestamp > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSaveAndLoadFilters() {
|
||||
val (cache, _) = createCache()
|
||||
cache.saveFilters("default", agegroup = "U13 (Elite)", subgroup = "U13 Elite")
|
||||
|
||||
val (agegroup, subgroup) = cache.loadFilters("default")
|
||||
assertEquals("U13 (Elite)", agegroup)
|
||||
assertEquals("U13 Elite", subgroup)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSaveFiltersWithNulls() {
|
||||
val (cache, _) = createCache()
|
||||
cache.saveFilters("default", agegroup = "U13", subgroup = "Elite")
|
||||
cache.saveFilters("default", agegroup = null, subgroup = null)
|
||||
|
||||
val (agegroup, subgroup) = cache.loadFilters("default")
|
||||
assertNull(agegroup)
|
||||
assertNull(subgroup)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClearCache() {
|
||||
val (cache, _) = createCache()
|
||||
cache.saveEvents("default", listOf(sampleEvent("1")))
|
||||
cache.saveFilters("default", "U13", "Elite")
|
||||
|
||||
cache.clearCache("default")
|
||||
|
||||
assertNull(cache.loadEvents("default"))
|
||||
val (agegroup, subgroup) = cache.loadFilters("default")
|
||||
assertNull(agegroup)
|
||||
assertNull(subgroup)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClearAllCache() {
|
||||
val (cache, _) = createCache()
|
||||
cache.saveEvents("default", listOf(sampleEvent("1")))
|
||||
cache.saveEvents("leonard", listOf(sampleEvent("2")))
|
||||
|
||||
cache.clearAllCache()
|
||||
|
||||
assertNull(cache.loadEvents("default"))
|
||||
assertNull(cache.loadEvents("leonard"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTypeFilterPersistence() {
|
||||
val (cache, _) = createCache()
|
||||
cache.saveTypeFilter("default", "GAMES")
|
||||
assertEquals("GAMES", cache.loadTypeFilter("default"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTypeFilterNullWhenNotSet() {
|
||||
val (cache, _) = createCache()
|
||||
assertNull(cache.loadTypeFilter("default"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package ch.parano.myicek.filter
|
||||
|
||||
import ch.parano.myicek.models.Event
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class EventFilterTest {
|
||||
|
||||
private fun event(
|
||||
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 testExtractSubgroupWithSeparator() {
|
||||
assertEquals("U13 Elite", extractSubgroup("U13 Elite - HC Ajoie"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractSubgroupMultipleSeparators() {
|
||||
assertEquals("U13 Elite - Group A", extractSubgroup("U13 Elite - Group A - HC Ajoie"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractSubgroupNoSeparator() {
|
||||
assertEquals("U13 Elite", extractSubgroup("U13 Elite"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractSubgroupEmptyString() {
|
||||
assertEquals("", extractSubgroup(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilterGamesOnly() {
|
||||
val events = listOf(
|
||||
event(id = "1", eventType = "Jeu"),
|
||||
event(id = "2", eventType = ""),
|
||||
event(id = "3", eventType = "Jeu"),
|
||||
)
|
||||
val result = filterEvents(events, agegroup = null, subgroup = null, typeFilter = TypeFilter.GAMES)
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("1", result[0].idEvent)
|
||||
assertEquals("3", result[1].idEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilterPracticesOnly() {
|
||||
val events = listOf(
|
||||
event(id = "1", eventType = "Jeu"),
|
||||
event(id = "2", eventType = ""),
|
||||
event(id = "3", eventType = ""),
|
||||
)
|
||||
val result = filterEvents(events, agegroup = null, subgroup = null, typeFilter = TypeFilter.PRACTICES)
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("2", result[0].idEvent)
|
||||
assertEquals("3", result[1].idEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilterAll() {
|
||||
val events = listOf(
|
||||
event(id = "1", eventType = "Jeu"),
|
||||
event(id = "2", eventType = ""),
|
||||
)
|
||||
val result = filterEvents(events, agegroup = null, subgroup = null, typeFilter = TypeFilter.ALL)
|
||||
assertEquals(2, result.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilterByAgegroup() {
|
||||
val events = listOf(
|
||||
event(id = "1", agegroup = "U13 (Elite)"),
|
||||
event(id = "2", agegroup = "U15"),
|
||||
event(id = "3", agegroup = "U13 (Elite)"),
|
||||
)
|
||||
val result = filterEvents(events, agegroup = "U13 (Elite)", subgroup = null, typeFilter = TypeFilter.ALL)
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("1", result[0].idEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilterBySubgroup() {
|
||||
val events = listOf(
|
||||
event(id = "1", name = "U13 Elite - HC Ajoie"),
|
||||
event(id = "2", name = "U13 Top - HC Geneva"),
|
||||
event(id = "3", name = "U13 Elite - HC Lausanne"),
|
||||
)
|
||||
val result = filterEvents(events, agegroup = null, subgroup = "U13 Elite", typeFilter = TypeFilter.ALL)
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("1", result[0].idEvent)
|
||||
assertEquals("3", result[1].idEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilterCombinedAgegroupAndSubgroup() {
|
||||
val events = listOf(
|
||||
event(id = "1", agegroup = "U13 (Elite)", name = "U13 Elite - HC Ajoie"),
|
||||
event(id = "2", agegroup = "U13 (Elite)", name = "U13 Top - HC Geneva"),
|
||||
event(id = "3", agegroup = "U15", name = "U13 Elite - HC Lausanne"),
|
||||
)
|
||||
val result = filterEvents(events, agegroup = "U13 (Elite)", subgroup = "U13 Elite", typeFilter = TypeFilter.ALL)
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("1", result[0].idEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilterGamesAndAgegroup() {
|
||||
val events = listOf(
|
||||
event(id = "1", agegroup = "U13 (Elite)", eventType = "Jeu"),
|
||||
event(id = "2", agegroup = "U13 (Elite)", eventType = ""),
|
||||
event(id = "3", agegroup = "U15", eventType = "Jeu"),
|
||||
)
|
||||
val result = filterEvents(events, agegroup = "U13 (Elite)", subgroup = null, typeFilter = TypeFilter.GAMES)
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("1", result[0].idEvent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDistinctAgegroups() {
|
||||
val events = listOf(
|
||||
event(agegroup = "U15"),
|
||||
event(agegroup = "U13 (Elite)"),
|
||||
event(agegroup = "U15"),
|
||||
)
|
||||
val result = events.distinctAgegroups()
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("U13 (Elite)", result[0])
|
||||
assertEquals("U15", result[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDistinctSubgroups() {
|
||||
val events = listOf(
|
||||
event(name = "U13 Elite - HC Ajoie"),
|
||||
event(name = "U13 Top - HC Geneva"),
|
||||
event(name = "U13 Elite - HC Lausanne"),
|
||||
)
|
||||
val result = events.distinctSubgroups()
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("U13 Elite", result[0])
|
||||
assertEquals("U13 Top", result[1])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package ch.parano.myicek.models
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ModelSerializationTest {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Test
|
||||
fun testAccountDeserialization() {
|
||||
val input = """{"name":"default","label":"Default"}"""
|
||||
val account = json.decodeFromString<Account>(input)
|
||||
assertEquals("default", account.name)
|
||||
assertEquals("Default", account.label)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEventDeserialization() {
|
||||
val input = """
|
||||
{
|
||||
"id_event": 761040,
|
||||
"agegroup": "U13 (Elite)",
|
||||
"name": "U13 Elite - HC Ajoie",
|
||||
"title": "U13 (Elite)\nGame\nRaiffeisen Arena",
|
||||
"opponent": "HC Ajoie",
|
||||
"place": "Raiffeisen Arena, Porrentruy",
|
||||
"start": "2024-11-10T14:00:00",
|
||||
"end": "2024-11-10T16:15:00",
|
||||
"color": "#e4222e",
|
||||
"event": "Jeu"
|
||||
}
|
||||
""".trimIndent()
|
||||
val event = json.decodeFromString<Event>(input)
|
||||
assertEquals("761040", event.idEvent)
|
||||
assertEquals("U13 (Elite)", event.agegroup)
|
||||
assertEquals("U13 Elite - HC Ajoie", event.name)
|
||||
assertEquals("HC Ajoie", event.opponent)
|
||||
assertEquals("#e4222e", event.color)
|
||||
assertEquals("Jeu", event.eventType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEventDeserializationWithNullFields() {
|
||||
val input = """
|
||||
{
|
||||
"id_event": "12345",
|
||||
"agegroup": "U15",
|
||||
"name": "",
|
||||
"title": "Practice",
|
||||
"opponent": "",
|
||||
"place": "Les Vernets",
|
||||
"start": "2024-11-10T14:00:00",
|
||||
"end": "2024-11-10T15:30:00"
|
||||
}
|
||||
""".trimIndent()
|
||||
val event = json.decodeFromString<Event>(input)
|
||||
assertEquals("12345", event.idEvent)
|
||||
assertEquals("", event.name)
|
||||
assertEquals("", event.opponent)
|
||||
assertNull(event.color)
|
||||
assertEquals("", event.eventType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPlayerWithIntNumber() {
|
||||
val input = """
|
||||
{"position":"Gardien","number":1,"fname":"Jean","lname":"Dupont","dob":"2012-03-15"}
|
||||
""".trimIndent()
|
||||
val player = json.decodeFromString<Player>(input)
|
||||
assertEquals("Gardien", player.position)
|
||||
assertEquals("1", player.number)
|
||||
assertEquals("Jean", player.fname)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPlayerWithStringNumber() {
|
||||
val input = """
|
||||
{"position":"Attaquant","number":"27","fname":"Pierre","lname":"Martin","dob":"2012-05-20"}
|
||||
""".trimIndent()
|
||||
val player = json.decodeFromString<Player>(input)
|
||||
assertEquals("27", player.number)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPlayerWithNullNumber() {
|
||||
val input = """
|
||||
{"position":null,"number":null,"fname":"Paul","lname":"Durand","dob":"2012-01-10"}
|
||||
""".trimIndent()
|
||||
val player = json.decodeFromString<Player>(input)
|
||||
assertNull(player.position)
|
||||
assertNull(player.number)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStaffDeserialization() {
|
||||
val input = """
|
||||
{"role":"Coach","fname":"Jean","lname":"Dupont"}
|
||||
""".trimIndent()
|
||||
val staff = json.decodeFromString<Staff>(input)
|
||||
assertEquals("Coach", staff.role)
|
||||
assertEquals("Jean", staff.fname)
|
||||
assertEquals("Dupont", staff.lname)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConvocationDeserialization() {
|
||||
val input = """
|
||||
{
|
||||
"available": [
|
||||
{"position":"Gardien","number":1,"fname":"Jean","lname":"Dupont","dob":"2012-03-15"}
|
||||
],
|
||||
"staff": [
|
||||
{"role":"Coach","fname":"Marc","lname":"Blanc"}
|
||||
]
|
||||
}
|
||||
""".trimIndent()
|
||||
val convocation = json.decodeFromString<Convocation>(input)
|
||||
assertEquals(1, convocation.available.size)
|
||||
assertEquals("1", convocation.available[0].number)
|
||||
assertEquals(1, convocation.staff.size)
|
||||
assertEquals("Coach", convocation.staff[0].role)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConvocationWithEmptyArrays() {
|
||||
val input = """{"available":[],"staff":[]}"""
|
||||
val convocation = json.decodeFromString<Convocation>(input)
|
||||
assertEquals(0, convocation.available.size)
|
||||
assertEquals(0, convocation.staff.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEventDetailDeserialization() {
|
||||
val input = """
|
||||
{
|
||||
"title": "U13 (Elite) - Saison HC Ajoie",
|
||||
"type": "games",
|
||||
"place": "Raiffeisen Arena, Porrentruy",
|
||||
"time_start": "2024-11-10T14:00:00",
|
||||
"time_end": "2024-11-10T16:15:00",
|
||||
"is_away": "1",
|
||||
"convocation": {
|
||||
"available": [],
|
||||
"staff": []
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val detail = json.decodeFromString<EventDetail>(input)
|
||||
assertEquals("U13 (Elite) - Saison HC Ajoie", detail.title)
|
||||
assertEquals("games", detail.type)
|
||||
assertEquals("2024-11-10T14:00:00", detail.timeStart)
|
||||
assertEquals(0, detail.convocation.available.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUserInfoDeserialization() {
|
||||
val input = """{"email":"rene@luria.ch","sub":"abc123"}"""
|
||||
val userInfo = json.decodeFromString<UserInfo>(input)
|
||||
assertEquals("rene@luria.ch", userInfo.email)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEventRoundTripSerialization() {
|
||||
val event = Event(
|
||||
idEvent = "761040",
|
||||
agegroup = "U13 (Elite)",
|
||||
name = "U13 Elite - HC Ajoie",
|
||||
title = "U13 (Elite)\nGame\nArena",
|
||||
opponent = "HC Ajoie",
|
||||
place = "Arena",
|
||||
start = "2024-11-10T14:00:00",
|
||||
end = "2024-11-10T16:15:00",
|
||||
color = "#e4222e",
|
||||
eventType = "Jeu",
|
||||
)
|
||||
val jsonString = json.encodeToString(Event.serializer(), event)
|
||||
val decoded = json.decodeFromString<Event>(jsonString)
|
||||
assertEquals(event, decoded)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package ch.parano.myicek.network
|
||||
|
||||
import ch.parano.myicek.models.EventDetail
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.mock.MockEngine
|
||||
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.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ApiServiceTest {
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private fun mockClient(respondJson: String, requestUrl: String, expectedAuth: String? = "Bearer test-token"): HttpClient {
|
||||
val engine = MockEngine { request ->
|
||||
val actualUrl = if (request.url.encodedQuery.isEmpty()) {
|
||||
request.url.encodedPath
|
||||
} else {
|
||||
request.url.encodedPath + "?" + request.url.encodedQuery
|
||||
}
|
||||
assertEquals(requestUrl, actualUrl)
|
||||
if (expectedAuth != null) {
|
||||
assertEquals(expectedAuth, request.headers["Authorization"])
|
||||
}
|
||||
respond(
|
||||
content = respondJson,
|
||||
status = HttpStatusCode.OK,
|
||||
headers = headersOf(HttpHeaders.ContentType, "application/json"),
|
||||
)
|
||||
}
|
||||
return HttpClient(engine) {
|
||||
install(ContentNegotiation) { json(json) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetUserInfo() = runTest {
|
||||
AuthTokenHolder.token = "test-token"
|
||||
val client = mockClient(
|
||||
"""{"email":"rene@luria.ch","sub":"abc"}""",
|
||||
"/userinfo",
|
||||
)
|
||||
val api = ApiService(client)
|
||||
val result = api.getUserInfo()
|
||||
assertEquals("rene@luria.ch", result.email)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetAccounts() = runTest {
|
||||
AuthTokenHolder.token = "test-token"
|
||||
val client = mockClient(
|
||||
"""[{"name":"default","label":"Default"},{"name":"leonard","label":"Leonard"}]""",
|
||||
"/accounts",
|
||||
)
|
||||
val api = ApiService(client)
|
||||
val result = api.getAccounts()
|
||||
assertEquals(2, result.size)
|
||||
assertEquals("default", result[0].name)
|
||||
assertEquals("Leonard", result[1].label)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSchedule() = runTest {
|
||||
AuthTokenHolder.token = "test-token"
|
||||
val responseJson = """
|
||||
[{"id_event":"761040","agegroup":"U13 (Elite)","name":"U13 Elite - HC Ajoie","title":"U13 (Elite)\nGame\nArena","opponent":"HC Ajoie","place":"Arena","start":"2024-11-10T14:00:00","end":"2024-11-10T16:15:00","color":"#e4222e","event":"Jeu"}]
|
||||
""".trimIndent()
|
||||
val client = mockClient(responseJson, "/schedule?account=default")
|
||||
val api = ApiService(client)
|
||||
val result = api.getSchedule("default")
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("761040", result[0].idEvent)
|
||||
assertEquals("Jeu", result[0].eventType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetGameDetail() = runTest {
|
||||
AuthTokenHolder.token = "test-token"
|
||||
val responseJson = """
|
||||
{"title":"U13 - Game","type":"games","place":"Arena","time_start":"2024-11-10T14:00:00","time_end":"2024-11-10T16:15:00","convocation":{"available":[{"position":"Gardien","number":1,"fname":"Jean","lname":"Dupont","dob":"2012-03-15"}],"staff":[{"role":"Coach","fname":"Marc","lname":"Blanc"}]}}
|
||||
""".trimIndent()
|
||||
val client = mockClient(responseJson, "/game/761040?account=default")
|
||||
val api = ApiService(client)
|
||||
val result: EventDetail = api.getGameDetail("761040", "default")
|
||||
assertEquals("U13 - Game", result.title)
|
||||
assertEquals("games", result.type)
|
||||
assertEquals(1, result.convocation.available.size)
|
||||
assertEquals("1", result.convocation.available[0].number)
|
||||
assertEquals(1, result.convocation.staff.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoAuthTokenSendsNoAuthHeader() = runTest {
|
||||
AuthTokenHolder.token = null
|
||||
val engine = MockEngine { request ->
|
||||
assertNull(request.headers["Authorization"])
|
||||
respond(
|
||||
content = """{"email":""}""",
|
||||
status = HttpStatusCode.OK,
|
||||
headers = headersOf(HttpHeaders.ContentType, "application/json"),
|
||||
)
|
||||
}
|
||||
val client = HttpClient(engine) {
|
||||
install(ContentNegotiation) { json(json) }
|
||||
}
|
||||
val api = ApiService(client)
|
||||
val result = api.getUserInfo()
|
||||
assertEquals("", result.email)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package ch.parano.myicek.util
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class EventDateTimeFormatterTest {
|
||||
|
||||
@Test
|
||||
fun formatsSameDayEventAsDateFollowedByTimeRange() {
|
||||
val formatted = formatEventDateTime(
|
||||
start = "2024-11-10T14:00:00",
|
||||
end = "2024-11-10T16:15:00",
|
||||
)
|
||||
assertEquals("Dimanche 10 novembre 2024 de 14h à 16h15", formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatsSameDayEventWithSpaceSeparator() {
|
||||
val formatted = formatEventDateTime(
|
||||
start = "2026-08-09 10:00:00",
|
||||
end = "2026-08-09 12:00:00",
|
||||
)
|
||||
assertEquals("Dimanche 9 août 2026 de 10h à 12h", formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatsCrossingMidnightEventWithBothDates() {
|
||||
val formatted = formatEventDateTime(
|
||||
start = "2024-11-10T20:00:00",
|
||||
end = "2024-11-11T01:30:00",
|
||||
)
|
||||
assertEquals("Dimanche 10 novembre 2024 de 20h à 1h30 (Lundi 11 novembre 2024)", formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsRawStartWhenStartUnparseable() {
|
||||
val formatted = formatEventDateTime(start = "s", end = "2024-11-10T16:15:00")
|
||||
assertEquals("s - 2024-11-10T16:15:00", formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsRawEndWhenEndUnparseable() {
|
||||
val formatted = formatEventDateTime(start = "2024-11-10T14:00:00", end = "e")
|
||||
assertEquals("2024-11-10T14:00:00 - e", formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnsRawWhenBothUnparseable() {
|
||||
val formatted = formatEventDateTime(start = "s", end = "e")
|
||||
assertEquals("s - e", formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatsEventDateWithDayOfWeek() {
|
||||
val formatted = formatEventDate("2026-08-09T10:00:00")
|
||||
assertEquals("Dimanche 9 août 2026", formatted)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package ch.parano.myicek.viewmodel
|
||||
|
||||
import ch.parano.myicek.cache.ScheduleCache
|
||||
import ch.parano.myicek.filter.TypeFilter
|
||||
import ch.parano.myicek.models.Account
|
||||
import ch.parano.myicek.models.Event
|
||||
import ch.parano.myicek.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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user