All checks were successful
PR Checks / android-build (pull_request) Successful in 7m19s
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
121 lines
5.6 KiB
Kotlin
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)
|
|
}
|
|
}
|