Files
Android-app/app/src/test/java/com/runicgateway/app/ui/ContentViewModelTest.kt
wtclaude 4e3bb914ff
All checks were successful
PR Checks / android-build (pull_request) Successful in 7m19s
test(coverage): raise unit coverage past the 50% gate (phases 0-2)
Executes COVERAGE_PLAN.md phases 0-2 to clear the SonarQube new-code coverage
gate (was 16.4%, threshold 50%). Estimated new-code coverage after this change
is ~57%. 109 new tests across 19 files; full suite is 264 tests, all green.

Phase 0 — coverage exclusions (sonar-project.properties): drop code a JVM unit
test can't execute from the *coverage* denominator (still analysed for
bugs/smells) — pure-@Composable UI the `*Screen.kt` glob missed
(ui/components/**, BlockRenderer, ShardComponents), Android-framework glue
(push services, Keystore-backed Encrypted* stores, Hilt di/**).

Phase 1 — DTO serialization tests: AdminDto, PublicDto, WikiDto, PostDto/PageDto/
ContactDto, SsoDto, the shard board DTOs and player game-data DTOs, and the
mobile-auth request bodies — decode + encode + computed helpers
(isPublished/isMaintenance/ActorDto.label/ShardStatusDto.isOnline).

Phase 2 — ViewModel tests: a MainDispatcherRule harness + hand-written API fakes
(FakePublicApi/FakeAdminApi/FakePlayerShardApi/FakeShardStream) drive real
repositories into the ViewModels. Covers the admin (dashboard/content/moderation/
support), content (news/post/page/wiki/home/contact), player (characters/
vendors/character/my-houses) and shard-board (champs/guilds/governors/houses/
hub) ViewModels — load success/error, form validation, role/status-aware
feedback, and live-frame merging.

To make the shard boards testable, extract a small `ShardStream` interface from
`ShardStreamClient` (bound in NetworkModule) so `ShardRepository` depends on the
capability, not the OkHttp client — lets a fake stream replace the perpetual SSE
reconnect loop in tests. No production behaviour change.

Phases 3 (repositories) and 4 (core net/auth top-up) are follow-ups; the
deep-dependency auth family (Login/Account/TrustedDevices ViewModels,
AuthRepository) lands with them. See docs/android/COVERAGE_PLAN.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
2026-07-22 16:04:02 -05:00

121 lines
5.6 KiB
Kotlin

/*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.runicgateway.app.ui
import androidx.lifecycle.SavedStateHandle
import com.runicgateway.app.data.api.dto.PageDto
import com.runicgateway.app.data.api.dto.PostDto
import com.runicgateway.app.data.api.dto.StatusDto
import com.runicgateway.app.data.api.dto.WikiPageDto
import com.runicgateway.app.data.api.dto.WikiSummaryDto
import com.runicgateway.app.data.api.fake.FakePublicApi
import com.runicgateway.app.data.repository.ContentRepository
import com.runicgateway.app.data.repository.SettingsRepository
import com.runicgateway.app.data.repository.WikiRepository
import com.runicgateway.app.ui.home.HomeViewModel
import com.runicgateway.app.ui.navigation.Routes
import com.runicgateway.app.ui.news.NewsViewModel
import com.runicgateway.app.ui.news.PostViewModel
import com.runicgateway.app.ui.page.PageViewModel
import com.runicgateway.app.ui.wiki.WikiPageViewModel
import com.runicgateway.app.ui.wiki.WikiViewModel
import com.runicgateway.app.util.MainDispatcherRule
import com.runicgateway.app.util.httpError
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
/** ViewModels over the public content APIs (news, CMS pages, wiki, home status). */
class ContentViewModelTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
private val api = FakePublicApi()
private val content = ContentRepository(api)
private val wiki = WikiRepository(api)
private val settings = SettingsRepository(api)
// ── News hub ──────────────────────────────────────────────────────────
@Test fun newsLoadsSelectedCategory() {
api.posts = listOf(PostDto(id = 1, category = "news", title = "Hi"))
val vm = NewsViewModel(content)
assertTrue(vm.state.value is UiState.Success)
assertEquals(1, (vm.state.value as UiState.Success).data.size)
}
@Test fun newsSelectCategoryReloads() {
val vm = NewsViewModel(content)
api.posts = listOf(PostDto(id = 2, category = "newsletter", title = "N"))
vm.selectCategory(ContentRepository.PostCategory.NEWSLETTER)
assertEquals(ContentRepository.PostCategory.NEWSLETTER, vm.category.value)
assertEquals(1, (vm.state.value as UiState.Success).data.size)
}
@Test fun newsServerErrorIsUiError() {
api.error = httpError(500)
assertTrue(NewsViewModel(content).state.value is UiState.Error)
}
// ── Post detail (SavedStateHandle args) ─────────────────────────────────
@Test fun postDetailLoadsForKnownCategory() {
api.post = PostDto(id = 7, category = "news", title = "Update", body = "<p>x</p>")
val handle = SavedStateHandle(
mapOf(Routes.Args.CATEGORY to "news", Routes.Args.ID_OR_SLUG to "update"),
)
val vm = PostViewModel(content, handle)
assertEquals("Update", (vm.state.value as UiState.Success).data.title)
}
@Test fun postDetailUnknownCategoryIsNotFoundWithoutApiCall() {
val handle = SavedStateHandle(
mapOf(Routes.Args.CATEGORY to "bogus", Routes.Args.ID_OR_SLUG to "x"),
)
val state = PostViewModel(content, handle).state.value
assertTrue(state is UiState.Error)
assertEquals(ErrorKind.NOT_FOUND, (state as UiState.Error).kind)
}
// ── CMS page ────────────────────────────────────────────────────────────
@Test fun pageLoadsBySlug() {
api.page = PageDto(id = 3, slug = "about", title = "About")
val vm = PageViewModel(content, SavedStateHandle(mapOf(Routes.Args.SLUG to "about")))
assertEquals("About", (vm.state.value as UiState.Success).data.title)
}
@Test fun pageNotFoundIsUiError() {
api.error = httpError(404)
val vm = PageViewModel(content, SavedStateHandle(mapOf(Routes.Args.SLUG to "missing")))
assertEquals(ErrorKind.NOT_FOUND, (vm.state.value as UiState.Error).kind)
}
// ── Wiki index + detail ─────────────────────────────────────────────────
@Test fun wikiIndexLoadsAndTracksQuery() {
api.wikiPages = listOf(WikiSummaryDto(id = 1, slug = "pvp", title = "PvP"))
val vm = WikiViewModel(wiki)
assertTrue(vm.state.value is UiState.Success)
vm.onQueryChange("housing")
assertEquals("housing", vm.query.value)
}
@Test fun wikiPageLoadsBySlug() {
api.wikiPage = WikiPageDto(id = 9, slug = "housing", title = "Housing", body = "b")
val vm = WikiPageViewModel(wiki, SavedStateHandle(mapOf(Routes.Args.SLUG to "housing")))
assertEquals("Housing", (vm.state.value as UiState.Success).data.title)
}
// ── Home status ─────────────────────────────────────────────────────────
@Test fun homeLoadsStatus() {
api.status = StatusDto(mode = "maintenance")
val vm = HomeViewModel(settings)
assertTrue((vm.state.value as UiState.Success).data.isMaintenance)
}
@Test fun homeNetworkErrorIsUiError() {
api.error = java.io.IOException("offline")
val state = HomeViewModel(settings).state.value
assertEquals(ErrorKind.NETWORK, (state as UiState.Error).kind)
}
}