summaryrefslogtreecommitdiff
path: root/android-app/app/src/test/kotlin/com/example
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-15 06:52:23 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-15 06:52:23 +0000
commitcc76e4f3cc4e4d958f398ed2899a8d653815985b (patch)
tree74befb533f198c0e072bce567317924d051a4157 /android-app/app/src/test/kotlin/com/example
parentc3f1178d30de7f1c5c536d0863d547299f2ab54e (diff)
fix: move weather feature to org/terst/nav package directories
Package declarations were already org.terst.nav.* but files lived under com/example/androidapp/. Kotlin 2.0 (K2) compiler on CI fails when package declarations don't match directory structure during kapt stub generation. Moved all 20 files to their correct locations and renamed MainActivity (weather) -> WeatherActivity to avoid confusion with the nav app's MainActivity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'android-app/app/src/test/kotlin/com/example')
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/data/api/WeatherApiServiceTest.kt88
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/data/model/ForecastItemTest.kt57
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/data/model/WindArrowTest.kt49
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/data/repository/WeatherRepositoryTest.kt101
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt110
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/ui/MainViewModelTest.kt105
6 files changed, 0 insertions, 510 deletions
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/data/api/WeatherApiServiceTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/data/api/WeatherApiServiceTest.kt
deleted file mode 100644
index ac2a652..0000000
--- a/android-app/app/src/test/kotlin/com/example/androidapp/data/api/WeatherApiServiceTest.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.example.androidapp.data.api
-
-import com.example.androidapp.data.model.WeatherResponse
-import com.squareup.moshi.Moshi
-import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
-import kotlinx.coroutines.test.runTest
-import okhttp3.mockwebserver.MockResponse
-import okhttp3.mockwebserver.MockWebServer
-import org.junit.After
-import org.junit.Assert.*
-import org.junit.Before
-import org.junit.Test
-import retrofit2.Retrofit
-import retrofit2.converter.moshi.MoshiConverterFactory
-
-class WeatherApiServiceTest {
-
- private lateinit var mockServer: MockWebServer
- private lateinit var service: WeatherApiService
-
- @Before
- fun setUp() {
- mockServer = MockWebServer()
- mockServer.start()
-
- val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
- service = Retrofit.Builder()
- .baseUrl(mockServer.url("/"))
- .addConverterFactory(MoshiConverterFactory.create(moshi))
- .build()
- .create(WeatherApiService::class.java)
- }
-
- @After
- fun tearDown() {
- mockServer.shutdown()
- }
-
- @Test
- fun `getWeatherForecast sends correct query parameters`() = runTest {
- mockServer.enqueue(MockResponse().setBody(WEATHER_JSON).setResponseCode(200))
-
- service.getWeatherForecast(
- latitude = 37.5,
- longitude = -122.3,
- hourly = "windspeed_10m,winddirection_10m,temperature_2m,precipitation_probability,weathercode",
- forecastDays = 1,
- windSpeedUnit = "kn"
- )
-
- val request = mockServer.takeRequest()
- val url = request.requestUrl!!
- assertEquals("37.5", url.queryParameter("latitude"))
- assertEquals("-122.3", url.queryParameter("longitude"))
- assertEquals("kn", url.queryParameter("wind_speed_unit"))
- }
-
- @Test
- fun `getWeatherForecast parses response correctly`() = runTest {
- mockServer.enqueue(MockResponse().setBody(WEATHER_JSON).setResponseCode(200))
-
- val response = service.getWeatherForecast(37.5, -122.3)
- assertEquals(37.5, response.latitude, 0.01)
- assertEquals(2, response.hourly.time.size)
- assertEquals(15.0, response.hourly.windspeed10m[0], 0.01)
- assertEquals(270.0, response.hourly.winddirection10m[0], 0.01)
- assertEquals(18.5, response.hourly.temperature2m[0], 0.01)
- assertEquals(20, response.hourly.precipitationProbability[0])
- assertEquals(1, response.hourly.weathercode[0])
- }
-
- companion object {
- private val WEATHER_JSON = """
- {
- "latitude": 37.5,
- "longitude": -122.3,
- "hourly": {
- "time": ["2026-03-13T00:00", "2026-03-13T01:00"],
- "windspeed_10m": [15.0, 16.0],
- "winddirection_10m": [270.0, 275.0],
- "temperature_2m": [18.5, 18.0],
- "precipitation_probability": [20, 25],
- "weathercode": [1, 1]
- }
- }
- """.trimIndent()
- }
-}
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/data/model/ForecastItemTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/data/model/ForecastItemTest.kt
deleted file mode 100644
index f0a903f..0000000
--- a/android-app/app/src/test/kotlin/com/example/androidapp/data/model/ForecastItemTest.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.example.androidapp.data.model
-
-import org.junit.Assert.*
-import org.junit.Test
-
-class ForecastItemTest {
-
- private fun makeItem(windKt: Double = 10.0, precipPct: Int = 0, weatherCode: Int = 0) =
- ForecastItem(
- timeIso = "2026-03-13T12:00",
- windKt = windKt,
- windDirDeg = 180.0,
- tempC = 15.0,
- precipProbabilityPct = precipPct,
- weatherCode = weatherCode
- )
-
- @Test
- fun `ForecastItem stores all fields correctly`() {
- val item = makeItem(windKt = 12.5, precipPct = 30, weatherCode = 61)
- assertEquals("2026-03-13T12:00", item.timeIso)
- assertEquals(12.5, item.windKt, 0.001)
- assertEquals(30, item.precipProbabilityPct)
- assertEquals(61, item.weatherCode)
- }
-
- @Test
- fun `weatherDescription returns rain for code 61`() {
- val item = makeItem(weatherCode = 61)
- assertTrue(item.weatherDescription().contains("Rain", ignoreCase = true))
- }
-
- @Test
- fun `weatherDescription returns clear for code 0`() {
- val item = makeItem(weatherCode = 0)
- assertTrue(item.weatherDescription().contains("Clear", ignoreCase = true))
- }
-
- @Test
- fun `weatherDescription returns cloudy for code 2`() {
- val item = makeItem(weatherCode = 2)
- assertTrue(item.weatherDescription().contains("Cloud", ignoreCase = true))
- }
-
- @Test
- fun `isRainy returns true for rain codes 51 to 67`() {
- assertTrue(makeItem(weatherCode = 51).isRainy())
- assertTrue(makeItem(weatherCode = 63).isRainy())
- assertTrue(makeItem(weatherCode = 67).isRainy())
- }
-
- @Test
- fun `isRainy returns false for clear codes`() {
- assertFalse(makeItem(weatherCode = 0).isRainy())
- assertFalse(makeItem(weatherCode = 1).isRainy())
- }
-}
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/data/model/WindArrowTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/data/model/WindArrowTest.kt
deleted file mode 100644
index b61e6fb..0000000
--- a/android-app/app/src/test/kotlin/com/example/androidapp/data/model/WindArrowTest.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.example.androidapp.data.model
-
-import org.junit.Assert.*
-import org.junit.Test
-
-class WindArrowTest {
-
- @Test
- fun `WindArrow holds lat lon speed direction`() {
- val arrow = WindArrow(lat = 37.5, lon = -122.3, speedKt = 15.0, directionDeg = 270.0)
- assertEquals(37.5, arrow.lat, 0.001)
- assertEquals(-122.3, arrow.lon, 0.001)
- assertEquals(15.0, arrow.speedKt, 0.001)
- assertEquals(270.0, arrow.directionDeg, 0.001)
- }
-
- @Test
- fun `WindArrow with zero speed is calm`() {
- val arrow = WindArrow(lat = 0.0, lon = 0.0, speedKt = 0.0, directionDeg = 0.0)
- assertEquals(0.0, arrow.speedKt, 0.001)
- assertTrue(arrow.isCalm())
- }
-
- @Test
- fun `WindArrow isCalm returns false when speed above threshold`() {
- val arrow = WindArrow(lat = 0.0, lon = 0.0, speedKt = 5.0, directionDeg = 90.0)
- assertFalse(arrow.isCalm())
- }
-
- @Test
- fun `WindArrow direction 360 is normalised to 0`() {
- val arrow = WindArrow(lat = 0.0, lon = 0.0, speedKt = 10.0, directionDeg = 360.0)
- assertEquals(0.0, arrow.normalisedDirection(), 0.001)
- }
-
- @Test
- fun `WindArrow direction within 0 to 359 is unchanged`() {
- val arrow = WindArrow(lat = 0.0, lon = 0.0, speedKt = 10.0, directionDeg = 180.0)
- assertEquals(180.0, arrow.normalisedDirection(), 0.001)
- }
-
- @Test
- fun `WindArrow beaufortScale returns correct force for various speeds`() {
- assertEquals(0, WindArrow(0.0, 0.0, 0.0, 0.0).beaufortScale()) // calm
- assertEquals(1, WindArrow(0.0, 0.0, 2.0, 0.0).beaufortScale()) // light air
- assertEquals(3, WindArrow(0.0, 0.0, 9.0, 0.0).beaufortScale()) // gentle
- assertEquals(7, WindArrow(0.0, 0.0, 30.0, 0.0).beaufortScale()) // near gale
- }
-}
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/data/repository/WeatherRepositoryTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/data/repository/WeatherRepositoryTest.kt
deleted file mode 100644
index e1bf288..0000000
--- a/android-app/app/src/test/kotlin/com/example/androidapp/data/repository/WeatherRepositoryTest.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-package com.example.androidapp.data.repository
-
-import com.example.androidapp.data.api.MarineApiService
-import com.example.androidapp.data.api.WeatherApiService
-import com.example.androidapp.data.model.*
-import io.mockk.coEvery
-import io.mockk.mockk
-import kotlinx.coroutines.test.runTest
-import org.junit.Assert.*
-import org.junit.Before
-import org.junit.Test
-
-class WeatherRepositoryTest {
-
- private val weatherApi = mockk<WeatherApiService>()
- private val marineApi = mockk<MarineApiService>()
- private lateinit var repo: WeatherRepository
-
- private val weatherResponse = WeatherResponse(
- latitude = 37.5,
- longitude = -122.3,
- hourly = WeatherHourly(
- time = listOf("2026-03-13T00:00", "2026-03-13T01:00"),
- windspeed10m = listOf(15.0, 16.0),
- winddirection10m = listOf(270.0, 275.0),
- temperature2m = listOf(18.5, 18.0),
- precipitationProbability = listOf(20, 25),
- weathercode = listOf(1, 1)
- )
- )
-
- private val marineResponse = MarineResponse(
- latitude = 37.5,
- longitude = -122.3,
- hourly = MarineHourly(
- time = listOf("2026-03-13T00:00", "2026-03-13T01:00"),
- waveHeight = listOf(1.2, 1.1),
- waveDirection = listOf(250.0, 255.0),
- oceanCurrentVelocity = listOf(0.3, 0.4),
- oceanCurrentDirection = listOf(180.0, 185.0)
- )
- )
-
- @Before
- fun setUp() {
- repo = WeatherRepository(weatherApi, marineApi)
- }
-
- @Test
- fun `fetchForecastItems maps weather response to ForecastItem list`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } returns weatherResponse
- coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
-
- val result = repo.fetchForecastItems(37.5, -122.3)
-
- assertTrue(result.isSuccess)
- val items = result.getOrThrow()
- assertEquals(2, items.size)
- assertEquals("2026-03-13T00:00", items[0].timeIso)
- assertEquals(15.0, items[0].windKt, 0.001)
- assertEquals(270.0, items[0].windDirDeg, 0.001)
- assertEquals(18.5, items[0].tempC, 0.001)
- assertEquals(20, items[0].precipProbabilityPct)
- assertEquals(1, items[0].weatherCode)
- }
-
- @Test
- fun `fetchWindArrow returns WindArrow for first (current) hour`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } returns weatherResponse
- coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
-
- val result = repo.fetchWindArrow(37.5, -122.3)
-
- assertTrue(result.isSuccess)
- val arrow = result.getOrThrow()
- assertEquals(37.5, arrow.lat, 0.001)
- assertEquals(-122.3, arrow.lon, 0.001)
- assertEquals(15.0, arrow.speedKt, 0.001)
- assertEquals(270.0, arrow.directionDeg, 0.001)
- }
-
- @Test
- fun `fetchForecastItems returns failure when weather API throws`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } throws RuntimeException("Network error")
- coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
-
- val result = repo.fetchForecastItems(37.5, -122.3)
-
- assertTrue(result.isFailure)
- }
-
- @Test
- fun `fetchWindArrow returns failure when API throws`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } throws RuntimeException("Timeout")
- coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
-
- val result = repo.fetchWindArrow(37.5, -122.3)
-
- assertTrue(result.isFailure)
- }
-}
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt
deleted file mode 100644
index 54afc26..0000000
--- a/android-app/app/src/test/kotlin/com/example/androidapp/ui/LocationPermissionHandlerTest.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-package com.example.androidapp.ui
-
-import org.junit.Assert.*
-import org.junit.Test
-
-class LocationPermissionHandlerTest {
-
- // Convenience factory — callers override only the lambdas they care about.
- private fun makeHandler(
- checkGranted: () -> Boolean = { false },
- onGranted: () -> Unit = {},
- onDenied: () -> Unit = {},
- requestPermissions: () -> Unit = {}
- ) = LocationPermissionHandler(checkGranted, onGranted, onDenied, requestPermissions)
-
- // ── start() ──────────────────────────────────────────────────────────────
-
- @Test
- fun `start - permission already granted - calls onGranted without requesting`() {
- var onGrantedCalled = false
- var requestCalled = false
- makeHandler(
- checkGranted = { true },
- onGranted = { onGrantedCalled = true },
- requestPermissions = { requestCalled = true }
- ).start()
-
- assertTrue("onGranted should be called", onGrantedCalled)
- assertFalse("requestPermissions should NOT be called", requestCalled)
- }
-
- @Test
- fun `start - permission not granted - calls requestPermissions without calling onGranted`() {
- var onGrantedCalled = false
- var requestCalled = false
- makeHandler(
- checkGranted = { false },
- onGranted = { onGrantedCalled = true },
- requestPermissions = { requestCalled = true }
- ).start()
-
- assertFalse("onGranted should NOT be called", onGrantedCalled)
- assertTrue("requestPermissions should be called", requestCalled)
- }
-
- // ── onResult() ───────────────────────────────────────────────────────────
-
- @Test
- fun `onResult - fine location granted - calls onGranted`() {
- var onGrantedCalled = false
- makeHandler(onGranted = { onGrantedCalled = true }).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to true,
- "android.permission.ACCESS_COARSE_LOCATION" to false
- )
- )
- assertTrue("onGranted should be called when fine location is granted", onGrantedCalled)
- }
-
- @Test
- fun `onResult - coarse location granted - calls onGranted`() {
- var onGrantedCalled = false
- makeHandler(onGranted = { onGrantedCalled = true }).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to false,
- "android.permission.ACCESS_COARSE_LOCATION" to true
- )
- )
- assertTrue("onGranted should be called when coarse location is granted", onGrantedCalled)
- }
-
- @Test
- fun `onResult - both permissions granted - calls onGranted`() {
- var onGrantedCalled = false
- makeHandler(onGranted = { onGrantedCalled = true }).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to true,
- "android.permission.ACCESS_COARSE_LOCATION" to true
- )
- )
- assertTrue(onGrantedCalled)
- }
-
- @Test
- fun `onResult - all permissions denied - calls onDenied not onGranted`() {
- var onGrantedCalled = false
- var onDeniedCalled = false
- makeHandler(
- onGranted = { onGrantedCalled = true },
- onDenied = { onDeniedCalled = true }
- ).onResult(
- mapOf(
- "android.permission.ACCESS_FINE_LOCATION" to false,
- "android.permission.ACCESS_COARSE_LOCATION" to false
- )
- )
- assertFalse("onGranted should NOT be called", onGrantedCalled)
- assertTrue("onDenied should be called", onDeniedCalled)
- }
-
- @Test
- fun `onResult - empty grants (never ask again scenario) - calls onDenied`() {
- var onDeniedCalled = false
- makeHandler(onDenied = { onDeniedCalled = true }).onResult(emptyMap())
- assertTrue(
- "onDenied should be called for empty grants (never-ask-again)",
- onDeniedCalled
- )
- }
-}
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/ui/MainViewModelTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/ui/MainViewModelTest.kt
deleted file mode 100644
index cb5f6f9..0000000
--- a/android-app/app/src/test/kotlin/com/example/androidapp/ui/MainViewModelTest.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.example.androidapp.ui
-
-import app.cash.turbine.test
-import com.example.androidapp.data.model.ForecastItem
-import com.example.androidapp.data.model.WindArrow
-import com.example.androidapp.data.repository.WeatherRepository
-import io.mockk.coEvery
-import io.mockk.mockk
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.*
-import org.junit.After
-import org.junit.Assert.*
-import org.junit.Before
-import org.junit.Test
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class MainViewModelTest {
-
- private val testDispatcher = UnconfinedTestDispatcher()
- private val repo = mockk<WeatherRepository>()
- private lateinit var vm: MainViewModel
-
- private val sampleArrow = WindArrow(37.5, -122.3, 15.0, 270.0)
- private val sampleForecast = listOf(
- ForecastItem("2026-03-13T00:00", 15.0, 270.0, 18.5, 20, 1)
- )
-
- @Before
- fun setUp() {
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- fun tearDown() {
- Dispatchers.resetMain()
- }
-
- private fun makeVm() = MainViewModel(repo)
-
- @Test
- fun `initial uiState is Loading`() {
- coEvery { repo.fetchWindArrow(any(), any()) } coAnswers { Result.success(sampleArrow) }
- coEvery { repo.fetchForecastItems(any(), any()) } coAnswers { Result.success(sampleForecast) }
- vm = makeVm()
- // Before loadWeather() is called the state is Loading
- assertEquals(UiState.Loading, vm.uiState.value)
- }
-
- @Test
- fun `loadWeather success transitions to Success state`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
- vm = makeVm()
-
- vm.uiState.test {
- assertEquals(UiState.Loading, awaitItem())
- vm.loadWeather(37.5, -122.3)
- assertEquals(UiState.Success, awaitItem())
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun `loadWeather populates windArrow and forecast`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
- vm = makeVm()
- vm.loadWeather(37.5, -122.3)
-
- assertEquals(sampleArrow, vm.windArrow.value)
- assertEquals(sampleForecast, vm.forecast.value)
- }
-
- @Test
- fun `loadWeather arrow failure transitions to Error state`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.failure(RuntimeException("Net error"))
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
- vm = makeVm()
-
- vm.uiState.test {
- awaitItem() // Loading
- vm.loadWeather(37.5, -122.3)
- val state = awaitItem()
- assertTrue(state is UiState.Error)
- assertTrue((state as UiState.Error).message.contains("Net error"))
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun `loadWeather forecast failure transitions to Error state`() = runTest {
- coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
- coEvery { repo.fetchForecastItems(any(), any()) } returns Result.failure(RuntimeException("Timeout"))
- vm = makeVm()
-
- vm.uiState.test {
- awaitItem() // Loading
- vm.loadWeather(37.5, -122.3)
- val state = awaitItem()
- assertTrue(state is UiState.Error)
- cancelAndIgnoreRemainingEvents()
- }
- }
-}