refactor: migrate Gson serialization to kotlinx.serialization (#475)
* refactor: migrate Gson serialization to kotlinx.serialization * fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement * test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults * fix(chat): support flexible numeric and string decoding for message timestamps * fix(chat): sequentialize cache loading to avoid overwriting network state
This commit is contained in:
64 files changed
+863
-577
No files matched your search
@@ -1,19 +1,40 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import com.google.gson.Gson
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ModelSerializationTest {
|
||||
private val gson = Gson()
|
||||
private val json = OkHttpProvider.json
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_allFields() {
|
||||
val json =
|
||||
fun testSkillSerialization() {
|
||||
val skill =
|
||||
Skill(
|
||||
name = "weather",
|
||||
description = "Get current weather info",
|
||||
category = "utility",
|
||||
enabled = true,
|
||||
)
|
||||
val jsonStr = json.encodeToString(skill)
|
||||
val deserialized = json.decodeFromString<Skill>(jsonStr)
|
||||
assertEquals(skill.name, deserialized.name)
|
||||
assertEquals(skill.description, deserialized.description)
|
||||
assertEquals(skill.category, deserialized.category)
|
||||
assertEquals(skill.enabled, deserialized.enabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization() {
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": "weather",
|
||||
@@ -22,7 +43,7 @@ class ModelSerializationTest {
|
||||
"enabled": true
|
||||
}
|
||||
""".trimIndent()
|
||||
val skill = gson.fromJson(json, Skill::class.java)
|
||||
val skill = json.decodeFromString<Skill>(jsonStr)
|
||||
assertEquals("weather", skill.name)
|
||||
assertEquals("Get current weather info", skill.description)
|
||||
assertEquals("utility", skill.category)
|
||||
@@ -30,170 +51,145 @@ class ModelSerializationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_optionalFieldsNull() {
|
||||
val json =
|
||||
"""
|
||||
{
|
||||
"name": "light_control",
|
||||
"description": null,
|
||||
"category": null,
|
||||
"enabled": false
|
||||
}
|
||||
""".trimIndent()
|
||||
val skill = gson.fromJson(json, Skill::class.java)
|
||||
assertEquals("light_control", skill.name)
|
||||
assertNull(skill.description)
|
||||
assertNull(skill.category)
|
||||
assertEquals(false, skill.enabled)
|
||||
fun testCronJobSerialization() {
|
||||
val job =
|
||||
CronJob(
|
||||
id = "job-123",
|
||||
name = "Daily Backup",
|
||||
schedule = JsonPrimitive("0 0 * * *"),
|
||||
state = "active",
|
||||
last_run_status = null,
|
||||
next_run = null,
|
||||
)
|
||||
val jsonStr = json.encodeToString(job)
|
||||
val deserialized = json.decodeFromString<CronJob>(jsonStr)
|
||||
assertEquals(job.id, deserialized.id)
|
||||
assertEquals(job.name, deserialized.name)
|
||||
assertEquals(job.schedule, deserialized.schedule)
|
||||
assertEquals(job.state, deserialized.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCronJobDeserialization_allFields() {
|
||||
val json =
|
||||
fun testCronJobDeserialization() {
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"id": "job-123",
|
||||
"name": "Daily Backup",
|
||||
"schedule": "0 0 * * *",
|
||||
"state": "active",
|
||||
"last_run_status": "success",
|
||||
"next_run": "2026-06-16T00:00:00Z"
|
||||
"state": "active"
|
||||
}
|
||||
""".trimIndent()
|
||||
val job = gson.fromJson(json, CronJob::class.java)
|
||||
val job = json.decodeFromString<CronJob>(jsonStr)
|
||||
assertEquals("job-123", job.id)
|
||||
assertEquals("Daily Backup", job.name)
|
||||
assertEquals("0 0 * * *", job.schedule)
|
||||
assertEquals("0 0 * * *", job.scheduleText)
|
||||
assertEquals("active", job.state)
|
||||
assertEquals("success", job.last_run_status)
|
||||
assertEquals("2026-06-16T00:00:00Z", job.next_run)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCronJobDeserialization_optionalFieldsNull() {
|
||||
val json =
|
||||
"""
|
||||
{
|
||||
"id": "job-456",
|
||||
"name": "One-off Task",
|
||||
"schedule": null,
|
||||
"state": null,
|
||||
"last_run_status": null,
|
||||
"next_run": null
|
||||
}
|
||||
""".trimIndent()
|
||||
val job = gson.fromJson(json, CronJob::class.java)
|
||||
assertEquals("job-456", job.id)
|
||||
assertEquals("One-off Task", job.name)
|
||||
assertNull(job.schedule)
|
||||
assertNull(job.state)
|
||||
assertNull(job.last_run_status)
|
||||
assertNull(job.next_run)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToggleSkillRequestSerialization() {
|
||||
val request = ToggleSkillRequest(name = "weather", enabled = false)
|
||||
val json = gson.toJson(request)
|
||||
val jsonStr = json.encodeToString(request)
|
||||
// Ensure serialization produces correct JSON structure
|
||||
val parsed = gson.fromJson(json, Map::class.java)
|
||||
assertEquals("weather", parsed["name"])
|
||||
assertEquals(false, parsed["enabled"])
|
||||
val parsed = json.decodeFromString<Map<String, JsonElement>>(jsonStr)
|
||||
assertEquals("weather", (parsed["name"] as? JsonPrimitive)?.content)
|
||||
assertEquals(false, (parsed["enabled"] as? JsonPrimitive)?.booleanOrNull)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_missingRequiredFields() {
|
||||
val json = "{}"
|
||||
val skill = gson.fromJson(json, Skill::class.java)
|
||||
// Verify null-safety issue: Gson allows non-nullable 'name' to be null at runtime
|
||||
assertNull(skill.name)
|
||||
// Primitive boolean defaults to false in Gson/JVM when missing
|
||||
assertEquals(false, skill.enabled)
|
||||
val jsonStr = "{}"
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_explicitNullForNonNullable() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": null,
|
||||
"enabled": null
|
||||
}
|
||||
""".trimIndent()
|
||||
val skill = gson.fromJson(json, Skill::class.java)
|
||||
assertNull(skill.name)
|
||||
assertEquals(false, skill.enabled)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCronJobDeserialization_missingRequiredFields() {
|
||||
val json = "{}"
|
||||
val job = gson.fromJson(json, CronJob::class.java)
|
||||
assertNull(job.id)
|
||||
assertNull(job.name)
|
||||
val jsonStr = "{}"
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<CronJob>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionListResponseDeserialization_missingRequiredFields() {
|
||||
val json = "{}"
|
||||
val response = gson.fromJson(json, SessionListResponse::class.java)
|
||||
assertNull(response.sessions)
|
||||
val jsonStr = "{}"
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<SessionListResponse>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionInfoDeserialization_missingRequiredFields() {
|
||||
val json = "{}"
|
||||
val info = gson.fromJson(json, SessionInfo::class.java)
|
||||
assertNull(info.id)
|
||||
assertNull(info.title)
|
||||
val jsonStr = "{}"
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<SessionInfo>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionMessagesResponseDeserialization_missingRequiredFields() {
|
||||
val json = "{}"
|
||||
val response = gson.fromJson(json, SessionMessagesResponse::class.java)
|
||||
assertNull(response.messages)
|
||||
val jsonStr = "{}"
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<SessionMessagesResponse>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_malformedJson() {
|
||||
val json = "{\"name\": \"weather\","
|
||||
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
|
||||
gson.fromJson(json, Skill::class.java)
|
||||
val jsonStr = "{\"name\": \"weather\","
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_typeMismatchObject() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": "weather",
|
||||
"enabled": {}
|
||||
}
|
||||
""".trimIndent()
|
||||
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
|
||||
gson.fromJson(json, Skill::class.java)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_typeMismatchArray() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": "weather",
|
||||
"enabled": []
|
||||
}
|
||||
""".trimIndent()
|
||||
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
|
||||
gson.fromJson(json, Skill::class.java)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_missingName_causesNullOnNonNullableField() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"description": "Get current weather info",
|
||||
@@ -201,15 +197,14 @@ class ModelSerializationTest {
|
||||
"enabled": true
|
||||
}
|
||||
""".trimIndent()
|
||||
val skill = gson.fromJson(json, Skill::class.java)
|
||||
// Gson bypasses Kotlin null-safety constraints, resulting in null on non-nullable field
|
||||
val nameNullable: String? = skill.name
|
||||
assertNull(nameNullable)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_nullName_causesNullOnNonNullableField() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": null,
|
||||
@@ -218,14 +213,14 @@ class ModelSerializationTest {
|
||||
"enabled": true
|
||||
}
|
||||
""".trimIndent()
|
||||
val skill = gson.fromJson(json, Skill::class.java)
|
||||
val nameNullable: String? = skill.name
|
||||
assertNull(nameNullable)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSkillDeserialization_missingEnabled_usesJvmDefaultValue() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": "weather",
|
||||
@@ -233,14 +228,14 @@ class ModelSerializationTest {
|
||||
"category": "utility"
|
||||
}
|
||||
""".trimIndent()
|
||||
val skill = gson.fromJson(json, Skill::class.java)
|
||||
// Boolean primitive defaults to false
|
||||
assertEquals(false, skill.enabled)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCronJobDeserialization_missingId_causesNullOnNonNullableField() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"name": "Daily Backup",
|
||||
@@ -248,52 +243,52 @@ class ModelSerializationTest {
|
||||
"state": "active"
|
||||
}
|
||||
""".trimIndent()
|
||||
val job = gson.fromJson(json, CronJob::class.java)
|
||||
val idNullable: String? = job.id
|
||||
assertNull(idNullable)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<CronJob>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionListResponseDeserialization_missingSessions_causesNullOnNonNullableField() {
|
||||
val json = "{}"
|
||||
val response = gson.fromJson(json, SessionListResponse::class.java)
|
||||
val sessionsNullable: List<SessionInfo>? = response.sessions
|
||||
assertNull(sessionsNullable)
|
||||
val jsonStr = "{}"
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<SessionListResponse>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionInfoDeserialization_missingId_causesNullOnNonNullableField() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"title": "My Session",
|
||||
"created_at": "2026-06-15T12:00:00Z"
|
||||
}
|
||||
""".trimIndent()
|
||||
val info = gson.fromJson(json, SessionInfo::class.java)
|
||||
val idNullable: String? = info.id
|
||||
assertNull(idNullable)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<SessionInfo>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionMessagesResponseDeserialization_missingMessages_causesNullOnNonNullableField() {
|
||||
val json = "{}"
|
||||
val response = gson.fromJson(json, SessionMessagesResponse::class.java)
|
||||
val messagesNullable: List<SessionMessage>? = response.messages
|
||||
assertNull(messagesNullable)
|
||||
val jsonStr = "{}"
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<SessionMessagesResponse>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeserialization_malformedJson_throwsJsonSyntaxException() {
|
||||
val json = """{"name": "weather", "enabled": """
|
||||
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
|
||||
gson.fromJson(json, Skill::class.java)
|
||||
val jsonStr = """{"name": "weather", "enabled": """
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<Skill>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStatusResponseDeserialization_allFields() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"version": "1.0.0",
|
||||
@@ -312,7 +307,7 @@ class ModelSerializationTest {
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val response = gson.fromJson(json, StatusResponse::class.java)
|
||||
val response = json.decodeFromString<StatusResponse>(jsonStr)
|
||||
assertEquals("1.0.0", response.version)
|
||||
assertEquals(true, response.gateway_running)
|
||||
assertEquals(5, response.active_sessions)
|
||||
@@ -326,8 +321,8 @@ class ModelSerializationTest {
|
||||
|
||||
@Test
|
||||
fun testStatusResponseDeserialization_missingFields() {
|
||||
val json = "{}"
|
||||
val response = gson.fromJson(json, StatusResponse::class.java)
|
||||
val jsonStr = "{}"
|
||||
val response = json.decodeFromString<StatusResponse>(jsonStr)
|
||||
assertNull(response.version)
|
||||
assertNull(response.gateway_running)
|
||||
assertNull(response.active_sessions)
|
||||
@@ -337,7 +332,7 @@ class ModelSerializationTest {
|
||||
|
||||
@Test
|
||||
fun testStatusResponseDeserialization_gatewayPlatformsNullValue() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"gateway_platforms": {
|
||||
@@ -348,7 +343,7 @@ class ModelSerializationTest {
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val response = gson.fromJson(json, StatusResponse::class.java)
|
||||
val response = json.decodeFromString<StatusResponse>(jsonStr)
|
||||
val platforms = response.gateway_platforms
|
||||
assertNotNull(platforms)
|
||||
assertNull(platforms!!["android"])
|
||||
@@ -360,20 +355,20 @@ class ModelSerializationTest {
|
||||
|
||||
@Test
|
||||
fun testStatusResponseDeserialization_typeMismatchInGatewayPlatforms() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"gateway_platforms": "not_a_map"
|
||||
}
|
||||
""".trimIndent()
|
||||
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
|
||||
gson.fromJson(json, StatusResponse::class.java)
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<StatusResponse>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSystemStatsResponse_deserialization() {
|
||||
val json =
|
||||
val jsonStr =
|
||||
"""
|
||||
{
|
||||
"os": "Linux",
|
||||
@@ -402,7 +397,7 @@ class ModelSerializationTest {
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val response = gson.fromJson(json, SystemStatsResponse::class.java)
|
||||
val response = json.decodeFromString<SystemStatsResponse>(jsonStr)
|
||||
assertEquals("Linux", response.os)
|
||||
assertEquals("aarch64", response.arch)
|
||||
assertEquals("hermes-box", response.hostname)
|
||||
@@ -413,36 +408,31 @@ class ModelSerializationTest {
|
||||
assertEquals(1.5, response.load_avg?.get(0) ?: 0.0, 0.01)
|
||||
assertEquals(86400.0, response.uptime_seconds ?: 0.0, 0.01)
|
||||
assertEquals(50.0, response.memory?.percent ?: 0.0, 0.01)
|
||||
assertEquals(8388608, response.memory?.total)
|
||||
assertEquals(8388608L, response.memory?.total)
|
||||
assertEquals(50.0, response.disk?.percent ?: 0.0, 0.01)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToggleSkillRequestDeserialization_missingRequiredFields() {
|
||||
val json = """{"enabled":true}"""
|
||||
val request = gson.fromJson(json, ToggleSkillRequest::class.java)
|
||||
val nameNullable: String? = request.name
|
||||
assertNull(nameNullable)
|
||||
assertEquals(true, request.enabled)
|
||||
val jsonStr = """{"enabled":true}"""
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<ToggleSkillRequest>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToggleSkillRequestDeserialization_explicitNull() {
|
||||
val json = """{"name":null,"enabled":null}"""
|
||||
val request = gson.fromJson(json, ToggleSkillRequest::class.java)
|
||||
val nameNullable: String? = request.name
|
||||
assertNull(nameNullable)
|
||||
assertEquals(false, request.enabled)
|
||||
val jsonStr = """{"name":null,"enabled":null}"""
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<ToggleSkillRequest>(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToggleSkillRequestSerialization_withNullName() {
|
||||
val jsonInput = """{"name":null,"enabled":true}"""
|
||||
val request = gson.fromJson(jsonInput, ToggleSkillRequest::class.java)
|
||||
|
||||
val jsonOutput = gson.toJson(request)
|
||||
val parsed = gson.fromJson(jsonOutput, Map::class.java)
|
||||
assertFalse(parsed.containsKey("name"))
|
||||
assertEquals(true, parsed["enabled"])
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
json.decodeFromString<ToggleSkillRequest>(jsonInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -3,6 +3,7 @@ package com.m57.hermescontrol.data.remote
|
||||
import com.m57.hermescontrol.data.model.SessionMessage
|
||||
import com.m57.hermescontrol.data.model.StatusResponse
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
@@ -13,7 +14,7 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
/**
|
||||
* MockWebServer-based tests for [HermesApiService].
|
||||
@@ -35,7 +36,7 @@ class HermesApiServiceMockWebServerTest {
|
||||
Retrofit
|
||||
.Builder()
|
||||
.baseUrl(mockServer.url("/"))
|
||||
.addConverterFactory(GsonConverterFactory.create(OkHttpProvider.gson))
|
||||
.addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
.create(HermesApiService::class.java)
|
||||
}
|
||||
@@ -244,7 +245,7 @@ class HermesApiServiceMockWebServerTest {
|
||||
|
||||
assertEquals("user", messages[0].role)
|
||||
assertEquals("Hello Hermes", messages[0].content)
|
||||
assertEquals("1718000000", messages[0].timestamp)
|
||||
assertEquals("1718000000", messages[0].timestampText)
|
||||
|
||||
assertEquals("assistant", messages[1].role)
|
||||
assertEquals("Hi! How can I help?", messages[1].content)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.data.remote
|
||||
|
||||
import com.m57.hermescontrol.data.model.ToggleSkillRequest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
@@ -13,7 +14,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
class HermesApiServiceTest {
|
||||
private lateinit var mockWebServer: MockWebServer
|
||||
@@ -28,7 +29,7 @@ class HermesApiServiceTest {
|
||||
Retrofit
|
||||
.Builder()
|
||||
.baseUrl(mockWebServer.url("/"))
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
apiService = retrofit.create(HermesApiService::class.java)
|
||||
@@ -151,58 +152,49 @@ class HermesApiServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSkills_missingFieldsInResponse() =
|
||||
runTest {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""[{"description":"info"}]"""),
|
||||
)
|
||||
fun testGetSkills_missingFieldsInResponse() {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""[{"description":"info"}]"""),
|
||||
)
|
||||
|
||||
val response = apiService.getSkills()
|
||||
assertTrue(response.isSuccessful)
|
||||
val skills = response.body()
|
||||
assertEquals(1, skills?.size)
|
||||
val skill = skills?.get(0)
|
||||
org.junit.jupiter.api.Assertions
|
||||
.assertNull(skill?.name)
|
||||
assertEquals(false, skill?.enabled)
|
||||
assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
runTest {
|
||||
apiService.getSkills()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCronJobs_missingFieldsInResponse() =
|
||||
runTest {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""[{"schedule":"* * * * *"}]"""),
|
||||
)
|
||||
fun testGetCronJobs_missingFieldsInResponse() {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""[{"schedule":"* * * * *"}]"""),
|
||||
)
|
||||
|
||||
val response = apiService.getCronJobs()
|
||||
assertTrue(response.isSuccessful)
|
||||
val jobs = response.body()
|
||||
assertEquals(1, jobs?.size)
|
||||
val job = jobs?.get(0)
|
||||
org.junit.jupiter.api.Assertions
|
||||
.assertNull(job?.id)
|
||||
org.junit.jupiter.api.Assertions
|
||||
.assertNull(job?.name)
|
||||
assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
runTest {
|
||||
apiService.getCronJobs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSessions_missingSessionsField() =
|
||||
runTest {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""{}"""),
|
||||
)
|
||||
fun testGetSessions_missingSessionsField() {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""{}"""),
|
||||
)
|
||||
|
||||
val response = apiService.getSessions()
|
||||
assertTrue(response.isSuccessful)
|
||||
org.junit.jupiter.api.Assertions
|
||||
.assertNull(response.body()?.sessions)
|
||||
assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
runTest {
|
||||
apiService.getSessions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSessionMessages_apiReturnsMalformedJson_throwsException() {
|
||||
@@ -212,7 +204,7 @@ class HermesApiServiceTest {
|
||||
.setBody("""{"messages": ["""),
|
||||
)
|
||||
|
||||
assertThrows(java.io.EOFException::class.java) {
|
||||
assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
runTest {
|
||||
apiService.getSessionMessages("session_1")
|
||||
}
|
||||
@@ -220,19 +212,19 @@ class HermesApiServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSessionMessages_missingMessagesField() =
|
||||
runTest {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""{}"""),
|
||||
)
|
||||
fun testGetSessionMessages_missingMessagesField() {
|
||||
mockWebServer.enqueue(
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setBody("""{}"""),
|
||||
)
|
||||
|
||||
val response = apiService.getSessionMessages("session_1")
|
||||
assertTrue(response.isSuccessful)
|
||||
org.junit.jupiter.api.Assertions
|
||||
.assertNull(response.body()?.messages)
|
||||
assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
runTest {
|
||||
apiService.getSessionMessages("session_1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSkills_malformedJsonResponse_throwsException() =
|
||||
@@ -243,7 +235,7 @@ class HermesApiServiceTest {
|
||||
.setBody("""[{"name":"weather""""),
|
||||
)
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(java.io.EOFException::class.java) {
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
kotlinx.coroutines.runBlocking {
|
||||
apiService.getSkills()
|
||||
}
|
||||
@@ -259,7 +251,7 @@ class HermesApiServiceTest {
|
||||
.setBody("""[{"name":"weather","enabled":{}}]"""),
|
||||
)
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
|
||||
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
|
||||
kotlinx.coroutines.runBlocking {
|
||||
apiService.getSkills()
|
||||
}
|
||||
|
||||
@@ -3,12 +3,31 @@ package com.m57.hermescontrol.data.ws
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@Suppress("FunctionName")
|
||||
private fun createJsonRpcResponse(
|
||||
jsonrpc: String,
|
||||
id: String?,
|
||||
result: Any? = null,
|
||||
error: JsonRpcError? = null,
|
||||
method: String? = null,
|
||||
params: Any? = null,
|
||||
): JsonRpcResponse =
|
||||
JsonRpcResponse(
|
||||
jsonrpc = jsonrpc,
|
||||
id = id,
|
||||
result = result?.toJsonElement(),
|
||||
error = error,
|
||||
method = method,
|
||||
params = params?.toJsonElement() as? JsonObject,
|
||||
)
|
||||
|
||||
class EventParserTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -24,7 +43,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseRpcResult_returnsRpcResultEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = "123",
|
||||
result = mapOf("status" to "success"),
|
||||
@@ -43,7 +62,7 @@ class EventParserTest {
|
||||
fun testParseRpcError_returnsRpcErrorEvent() {
|
||||
val error = JsonRpcError(code = -32600, message = "Invalid Request")
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = "456",
|
||||
result = null,
|
||||
@@ -62,7 +81,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseGatewayReady_returnsGatewayReadyEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -83,7 +102,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseMessageToken_returnsMessageTokenEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -105,7 +124,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseMessageToken_withSessionIdInParams_returnsMessageTokenEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -128,7 +147,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseThinkingDelta_returnsThinkingDeltaEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -150,7 +169,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseClarifyRequest_returnsClarifyRequestEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -176,7 +195,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseClarifyRequest_withQuestionFields_parsesSuccessfully() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -204,7 +223,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseSessionInfo_returnsSessionInfoEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -220,7 +239,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseMessageStart_returnsMessageStartEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -241,7 +260,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseMessageDelta_returnsMessageTokenEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -258,7 +277,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseMessageComplete_returnsMessageCompleteEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -274,7 +293,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseMessageDone_returnsMessageDoneEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -289,7 +308,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseToolStart_returnsToolStartEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -311,7 +330,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseToolComplete_returnsToolCompleteEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -333,7 +352,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseStatusUpdate_returnsStatusUpdateEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -349,7 +368,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseSessionUpdated_returnsSessionUpdatedEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -365,7 +384,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseUnknownType_returnsUnknownEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -380,7 +399,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseNullParams_returnsUnknownEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
@@ -395,7 +414,7 @@ class EventParserTest {
|
||||
@Test
|
||||
fun testParseNullTypeInParams_returnsUnknownEvent() {
|
||||
val response =
|
||||
JsonRpcResponse(
|
||||
createJsonRpcResponse(
|
||||
jsonrpc = "2.0",
|
||||
id = null,
|
||||
result = null,
|
||||
|
||||
Reference in new issue
Block a user