Upload via GUI (7592 Dateien)

This commit is contained in:
2026-10-16 19:07:55 +00:00
parent 7f397d596c
commit 893bef356c
34 changed files with 11604 additions and 0 deletions
@@ -0,0 +1,21 @@
package de.deskhub.staff.data.remote
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ConnectionMonitorTest {
@After
fun reset() = ConnectionMonitor.set(false)
@Test
fun `starts online and follows set`() {
ConnectionMonitor.set(false)
assertFalse(ConnectionMonitor.offline.value)
ConnectionMonitor.set(true)
assertTrue(ConnectionMonitor.offline.value)
ConnectionMonitor.set(false)
assertFalse(ConnectionMonitor.offline.value)
}
}
@@ -0,0 +1,29 @@
package de.deskhub.staff.data.remote
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
class DeskHubUrlTest {
@Test
fun `adds https and preserves installation path`() {
assertEquals(
"https://support.example.de/deskhub",
DeskHubUrl.normalize("support.example.de/deskhub/"),
)
}
@Test
fun `rejects cleartext server`() {
assertThrows(IllegalArgumentException::class.java) {
DeskHubUrl.normalize("http://support.example.de")
}
}
@Test
fun `rejects credentials and query parameters`() {
assertThrows(IllegalArgumentException::class.java) {
DeskHubUrl.normalize("https://user@example.de?token=secret")
}
}
}
@@ -0,0 +1,84 @@
package de.deskhub.staff.data.remote.dto
import com.google.gson.Gson
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
/** Prüft, dass typische Server-Antworten (Format der PHP-API) korrekt gelesen werden. */
class ApiDtoParsingTest {
private val gson = Gson()
@Test
fun `metadata contains health issues and permissions`() {
val json = """{"staff":{"id":3,"name":"Max","email":"m@x.de","role":"agent","permissions":{"view_reports":true,"reply_ticket":false}},
"departments":[{"id":1,"name":"Technik"}],"priorities":[],"agents":[],"statuses":[],"tags":[],"liveChatEnabled":true,
"health":[{"level":"error","code":"mailbox_down","message":"Postfach nicht erreichbar"}]}"""
val m = gson.fromJson(json, MetadataResponse::class.java)
assertEquals("Max", m.staff.name)
assertTrue(m.staff.permissions["view_reports"] == true)
assertFalse(m.staff.permissions["reply_ticket"] == true)
assertEquals(1, m.health.size)
assertEquals("mailbox_down", m.health[0].code)
assertEquals("error", m.health[0].level)
}
@Test
fun `old server without health or permissions still parses`() {
val m = gson.fromJson("""{"staff":{"id":1,"name":"A","email":"a@x.de","role":"admin"}}""", MetadataResponse::class.java)
assertTrue(m.health.isEmpty())
assertTrue(m.staff.permissions.isEmpty())
val t = gson.fromJson("""{"id":5,"number":"T-1","subject":"S"}""", TicketDetailDto::class.java)
assertNull(t.permissions)
assertTrue(t.threads.isEmpty())
}
@Test
fun `ticket detail carries per ticket permissions`() {
val t = gson.fromJson("""{"id":5,"number":"T-1","subject":"S","permissions":{"reply_ticket":true,"close_ticket":false}}""", TicketDetailDto::class.java)
assertEquals(true, t.permissions?.get("reply_ticket"))
assertEquals(false, t.permissions?.get("close_ticket"))
}
@Test
fun `activity response reports health`() {
val a = gson.fromJson("""{"newReplies":2,"newUnassigned":1,"serverTime":"2026-10-15 10:00:00","health":[{"level":"warning","code":"fetch_stale","message":"Abruf veraltet"}]}""", ActivityResponseDto::class.java)
assertEquals(2, a.newReplies)
assertEquals("fetch_stale", a.health.single().code)
}
@Test
fun `saved views parse filters`() {
val v = gson.fromJson("""{"items":[{"id":1,"name":"Überfällige","isShared":true,"mine":false,"ownerName":"Eva","filters":{"status":"open","sla":"overdue"}}]}""", SavedViewListResponse::class.java)
val view = v.items.single()
assertEquals("Überfällige", view.name)
assertTrue(view.isShared)
assertFalse(view.mine)
assertEquals("overdue", view.filters["sla"])
}
@Test
fun `sla plans parse`() {
val p = gson.fromJson("""{"items":[{"id":2,"name":"Premium","responseHours":2,"resolutionHours":8,"isActive":true,"departmentCount":1,"helpTopicCount":3}]}""", SlaPlanListResponse::class.java)
assertEquals(8, p.items.single().resolutionHours)
assertEquals(3, p.items.single().helpTopicCount)
}
@Test
fun `report response parses rows and totals`() {
val r = gson.fromJson("""{"config":{"dimension":"department"},"columns":[{"key":"label","label":"Abteilung"},{"key":"total","label":"Tickets"}],
"rows":[{"label":"Technik","total":42}],"totals":{"total":42},"dimensions":[],"metrics":[],"saved":[{"id":1,"name":"Monat","isShared":true,"mine":true}]}""", ReportResponseDto::class.java)
assertEquals(2, r.columns.size)
assertEquals("Technik", r.rows.single()["label"])
assertEquals("Monat", r.saved.single().name)
}
@Test
fun `error envelope falls back to defaults`() {
val e = gson.fromJson("""{"error":{"code":"FORBIDDEN","message":"Keine Berechtigung."}}""", ErrorEnvelope::class.java)
assertEquals("FORBIDDEN", e.error.code)
assertEquals("UNKNOWN", gson.fromJson("""{"error":{}}""", ErrorEnvelope::class.java).error.code)
}
}
@@ -0,0 +1,88 @@
package de.deskhub.staff.ui
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import de.deskhub.staff.domain.model.HealthIssue
import de.deskhub.staff.domain.model.Metadata
import de.deskhub.staff.domain.model.Staff
import de.deskhub.staff.domain.model.StatusOption
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/** Compose-Oberflächentests auf der JVM (Robolectric) – kein Emulator nötig. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class ComposeUiTest {
@get:Rule
val rule = createComposeRule()
@Test
fun offlineBannerAppearsAndRetryWorks() {
var retried = 0
rule.setContent {
MaterialTheme { StatusBanners(DeskHubState(offline = true), onRetry = { retried++ }) }
}
rule.onNodeWithText("Keine Verbindung zum Server", substring = true).assertIsDisplayed()
rule.onNodeWithText("Erneut").performClick()
assertEquals(1, retried)
}
@Test
fun noBannerWhenOnlineAndHealthy() {
rule.setContent { MaterialTheme { StatusBanners(DeskHubState(offline = false), onRetry = {}) } }
rule.onNodeWithText("Keine Verbindung zum Server", substring = true).assertDoesNotExist()
rule.onNodeWithText("Störung", substring = true).assertDoesNotExist()
}
@Test
fun serverHealthIssuesAreShownAsFaultOrHint() {
rule.setContent {
MaterialTheme {
StatusBanners(
DeskHubState(healthIssues = listOf(HealthIssue("error", "Postfach nicht erreichbar"), HealthIssue("warning", "Abruf veraltet"))),
onRetry = {},
)
}
}
rule.onNodeWithText("Störung: Postfach nicht erreichbar").assertIsDisplayed()
rule.onNodeWithText("Hinweis: Abruf veraltet").assertIsDisplayed()
}
@Test
fun statusPillShowsLabelFromMetadata() {
val staff = Staff(id = 1, name = "Max", email = "m@x.de", role = "agent", permissions = emptyMap())
val meta = Metadata(staff, emptyList(), emptyList(), emptyList(), listOf(StatusOption("open", "Offen", "#3b82f6")))
rule.setContent {
MaterialTheme {
StatusPill("open", meta)
StatusPill("custom_state", meta)
}
}
rule.onNodeWithText("Offen").assertIsDisplayed()
rule.onNodeWithText("custom_state").assertIsDisplayed() // unbekannter Status: Rohwert statt Absturz
}
@Test
fun themeMenuOffersAllModesAndReportsSelection() {
var chosen: ThemeMode? = null
rule.setContent {
MaterialTheme {
CompositionLocalProvider(LocalThemeController provides ThemeController(ThemeMode.System) { chosen = it }) { ThemeMenuButton() }
}
}
rule.onNodeWithContentDescription("Design wechseln").performClick()
listOf("Wie System", "Hell", "Dunkel").forEach { rule.onNodeWithText(it).assertIsDisplayed() }
rule.onNodeWithText("Dunkel").performClick()
assertTrue(chosen == ThemeMode.Dark)
}
}
@@ -0,0 +1,27 @@
package de.deskhub.staff.ui
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class PermissionsTest {
@Test
fun `ticket permission overrides global permission`() {
// Globales Recht vorhanden, aber in der Abteilung dieses Tickets nicht erlaubt
assertFalse(ticketPermission(mapOf("reply_ticket" to false), mapOf("reply_ticket" to true), "reply_ticket"))
// Globales Recht fehlt, aber die Abteilungsrolle erlaubt es
assertTrue(ticketPermission(mapOf("close_ticket" to true), mapOf("close_ticket" to false), "close_ticket"))
}
@Test
fun `falls back to global permissions for older servers`() {
assertTrue(ticketPermission(null, mapOf("post_note" to true), "post_note"))
assertFalse(ticketPermission(null, mapOf("post_note" to false), "post_note"))
}
@Test
fun `unknown permission is denied`() {
assertFalse(ticketPermission(emptyMap(), emptyMap(), "assign_ticket"))
assertFalse(ticketPermission(null, null, "assign_ticket"))
}
}