diff options
Diffstat (limited to 'android-app/app/src/test/kotlin/com/example/androidapp')
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() - } - } -} |
