feat: add SiteMetadataCache for per-account site metadata storage

SharedPreferences-based cache for GhostSite metadata keyed by account ID.
Supports save/get/getVersion/remove operations with Gson serialization.
Includes Robolectric tests for round-trip, overwrite, multi-account
isolation, and removal.
This commit is contained in:
Paweł Orzech 2026-03-20 00:26:46 +01:00
parent 6761eae351
commit 492ee1ca11
2 changed files with 203 additions and 0 deletions

View file

@ -0,0 +1,42 @@
package com.swoosh.microblog.data
import android.content.Context
import android.content.SharedPreferences
import com.google.gson.Gson
import com.swoosh.microblog.data.model.GhostSite
class SiteMetadataCache(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(
PREFS_NAME, Context.MODE_PRIVATE
)
private val gson = Gson()
fun save(accountId: String, site: GhostSite) {
val json = gson.toJson(site)
prefs.edit().putString(keyForAccount(accountId), json).apply()
}
fun get(accountId: String): GhostSite? {
val json = prefs.getString(keyForAccount(accountId), null) ?: return null
return try {
gson.fromJson(json, GhostSite::class.java)
} catch (e: Exception) {
null
}
}
fun getVersion(accountId: String): String? {
return get(accountId)?.version
}
fun remove(accountId: String) {
prefs.edit().remove(keyForAccount(accountId)).apply()
}
private fun keyForAccount(accountId: String): String = "site_$accountId"
companion object {
const val PREFS_NAME = "site_metadata_cache"
}
}

View file

@ -0,0 +1,161 @@
package com.swoosh.microblog.data
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import com.swoosh.microblog.data.model.GhostSite
import org.junit.Assert.*
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(application = Application::class)
class SiteMetadataCacheTest {
private lateinit var cache: SiteMetadataCache
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Application>()
cache = SiteMetadataCache(context)
}
@Test
fun `save and get round trip`() {
val site = GhostSite(
title = "My Blog",
description = "A test blog",
logo = "https://example.com/logo.png",
icon = "https://example.com/icon.png",
accent_color = "#ff1a75",
url = "https://example.com/",
version = "5.82.0",
locale = "en"
)
cache.save("account-1", site)
val retrieved = cache.get("account-1")
assertNotNull(retrieved)
assertEquals("My Blog", retrieved!!.title)
assertEquals("A test blog", retrieved.description)
assertEquals("https://example.com/logo.png", retrieved.logo)
assertEquals("https://example.com/icon.png", retrieved.icon)
assertEquals("#ff1a75", retrieved.accent_color)
assertEquals("https://example.com/", retrieved.url)
assertEquals("5.82.0", retrieved.version)
assertEquals("en", retrieved.locale)
}
@Test
fun `get returns null for unknown account`() {
val result = cache.get("nonexistent-account")
assertNull(result)
}
@Test
fun `getVersion returns version string`() {
val site = GhostSite(
title = "Blog",
description = null,
logo = null,
icon = null,
accent_color = null,
url = null,
version = "5.82.0",
locale = null
)
cache.save("account-2", site)
assertEquals("5.82.0", cache.getVersion("account-2"))
}
@Test
fun `getVersion returns null for unknown account`() {
assertNull(cache.getVersion("nonexistent"))
}
@Test
fun `save overwrites existing data`() {
val site1 = GhostSite(
title = "Old Title",
description = null,
logo = null,
icon = null,
accent_color = null,
url = null,
version = "5.0.0",
locale = null
)
val site2 = GhostSite(
title = "New Title",
description = "Updated",
logo = null,
icon = null,
accent_color = null,
url = null,
version = "5.82.0",
locale = null
)
cache.save("account-3", site1)
cache.save("account-3", site2)
val retrieved = cache.get("account-3")
assertEquals("New Title", retrieved?.title)
assertEquals("Updated", retrieved?.description)
assertEquals("5.82.0", retrieved?.version)
}
@Test
fun `different accounts have independent data`() {
val site1 = GhostSite(
title = "Blog One",
description = null,
logo = null,
icon = null,
accent_color = null,
url = null,
version = "5.0.0",
locale = null
)
val site2 = GhostSite(
title = "Blog Two",
description = null,
logo = null,
icon = null,
accent_color = null,
url = null,
version = "4.48.0",
locale = null
)
cache.save("account-a", site1)
cache.save("account-b", site2)
assertEquals("Blog One", cache.get("account-a")?.title)
assertEquals("Blog Two", cache.get("account-b")?.title)
}
@Test
fun `remove deletes cached data`() {
val site = GhostSite(
title = "To Remove",
description = null,
logo = null,
icon = null,
accent_color = null,
url = null,
version = "5.0.0",
locale = null
)
cache.save("account-remove", site)
assertNotNull(cache.get("account-remove"))
cache.remove("account-remove")
assertNull(cache.get("account-remove"))
}
}