- minor code clean up
 - Remove old api and response
 - Tests passed
 - Weather API successfully replaces
This commit is contained in:
2024-10-02 00:19:31 +01:00
committed by GitHub
parent 0aff414b1c
commit 54a7d523c0
65 changed files with 54166 additions and 1870 deletions

View File

@@ -18,8 +18,8 @@ android {
applicationId "com.appttude.h_mal.atlas_weather"
minSdkVersion MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION
versionCode 5
versionName "3.0"
versionCode 6
versionName "3.1"
testInstrumentationRunner "com.appttude.h_mal.atlas_weather.application.TestRunner"
vectorDrawables.useSupportLibrary = true
@@ -32,6 +32,10 @@ android {
buildConfigField "String", "ParamOne", System.getenv('WEATHER_API')
buildConfigField "String", "ParamTwo", System.getenv('SEARCH_API')
}
packagingOptions {
resources.excludes.add("META-INF/*")
}
}
android {
sourceSets {

View File

@@ -1,4 +0,0 @@
{
"cod": 401,
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."
}

View File

@@ -0,0 +1 @@
No account found with API key 'wrong api key'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -80,8 +80,8 @@ open class BaseTest<A : Activity>(
afterLaunch()
}
fun stubEndpoint(url: String, stub: Stubs, code: Int = 200) {
testApp.stubUrl(url, stub.id, code)
fun stubEndpoint(url: String, stub: Stubs, code: Int = 200, extension: String = ".json") {
testApp.stubUrl(url, stub.id, code, extension)
}
fun unstubEndpoint(url: String) {
@@ -125,7 +125,6 @@ open class BaseTest<A : Activity>(
})
}
@Suppress("DEPRECATION")
fun checkToastMessage(message: String) {
Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView)))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))

View File

@@ -17,7 +17,12 @@ import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import com.appttude.h_mal.atlas_weather.helpers.checkErrorMessage
import com.appttude.h_mal.atlas_weather.helpers.checkImage
@@ -60,7 +65,7 @@ open class BaseTestRobot {
.atPosition(position).perform(click())
}
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
@@ -73,7 +78,7 @@ open class BaseTestRobot {
fun <VH : ViewHolder> scrollToRecyclerItem(
recyclerId: Int,
resIdForString: Int
): ViewInteraction? {
): ViewInteraction {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
@@ -86,7 +91,7 @@ open class BaseTestRobot {
fun <VH : ViewHolder> scrollToRecyclerItemByPosition(
recyclerId: Int,
position: Int
): ViewInteraction? {
): ViewInteraction {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.

View File

@@ -16,9 +16,9 @@ class MockingNetworkInterceptor(
override fun intercept(chain: Interceptor.Chain): Response {
idlingResource.increment()
val original = chain.request()
val originalHttpUrl = original.url.toString().split("?")[0]
val originalHttpUrl = original.url.toString()
feedMap[originalHttpUrl]?.let { responsePair ->
feedMap[feedMap.keys.first { originalHttpUrl.contains(it) }]?.let { responsePair ->
val code = responsePair.second
val jsonBody = responsePair.first

View File

@@ -1,5 +1,6 @@
package com.appttude.h_mal.atlas_weather.utils
const val baseUrl = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/"
enum class Stubs(
val id: String
) {
@@ -7,5 +8,5 @@ enum class Stubs(
Imperial("valid_response_imperial"),
WrongLocation("wrong_location_response"),
InvalidKey("invalid_api_key_response"),
Sydney("valid_response_metric_sydney")
Sydney("valid_response_metric_sydney"),
}

View File

@@ -49,9 +49,9 @@ class TestAppClass : AppClass() {
return database
}
fun stubUrl(url: String, rawPath: String, code: Int = 200) {
fun stubUrl(url: String, rawPath: String, code: Int = 200, extension: String = ".json") {
val iStream =
InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath.json")
InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath$extension")
val data = iStream.bufferedReader().use(BufferedReader::readText)
mockingNetworkInterceptor.addUrlStub(url = url, data = data, code = code)
}

View File

@@ -13,6 +13,7 @@ import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.types.UnitType.Companion.getLabel
fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() }
@@ -24,10 +25,7 @@ class SettingsScreen : BaseTestRobot() {
RecyclerViewActions.actionOnItem<ViewHolder>(
ViewMatchers.hasDescendant(withText(R.string.weather_units)),
click()))
val label = when (unitType) {
UnitType.METRIC -> "Metric"
UnitType.IMPERIAL -> "Imperial"
}
val label = unitType.getLabel()
onView(withText(label))
.inRoot(isDialog())

View File

@@ -9,6 +9,7 @@ import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.robot.furtherInfoScreen
import com.appttude.h_mal.atlas_weather.robot.settingsScreen
import com.appttude.h_mal.atlas_weather.robot.weatherScreen
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import org.junit.Test
import tools.fastlane.screengrab.Screengrab
@@ -17,7 +18,7 @@ import tools.fastlane.screengrab.Screengrab
class SnapshotCaptureTest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubEndpoint(baseUrl, Stubs.Metric)
stubLocation("London", 51.51, -0.13)
clearPrefs()
}
@@ -50,7 +51,7 @@ class SnapshotCaptureTest : BaseTest<MainActivity>(MainActivity::class.java) {
openMenuItem()
}
settingsScreen {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Imperial)
stubEndpoint(baseUrl, Stubs.Imperial)
Screengrab.screenshot("SettingsScreen")
}
}

View File

@@ -1,36 +1,27 @@
package com.appttude.h_mal.atlas_weather.tests
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import org.junit.Test
class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.InvalidKey, 400)
}
@Test
fun loadApp_invalidKeyWeatherResponse_returnsEmptyViewPage() {
homeScreen {
waitFor(2000)
// verify empty
verifyUnableToRetrieve()
}
stubEndpoint(baseUrl, Stubs.InvalidKey, 400, ".txt")
}
@Test
fun invalidKeyWeatherResponse_swipeToRefresh_returnsValidPage() {
homeScreen {
waitFor(2000)
// verify empty
verifyUnableToRetrieve()
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubEndpoint(baseUrl, Stubs.Metric)
refresh()
verifyCurrentTemperature(2)
verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location")
}
}

View File

@@ -5,19 +5,20 @@ import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import org.junit.Test
class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubEndpoint(baseUrl, Stubs.Metric)
}
@Test
fun loadApp_validWeatherResponse_returnsValidPage() {
homeScreen {
isDisplayed()
verifyCurrentTemperature(2)
verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location")
}
}

View File

@@ -18,7 +18,6 @@ import org.kodein.di.LazyKodein
import java.io.BufferedReader
class TestAppClass : AppClass() {
override val kodein: LazyKodein = super.kodein
private val idlingResources = CountingIdlingResource("Data_loader")
@@ -53,9 +52,9 @@ class TestAppClass : AppClass() {
return database
}
fun stubUrl(url: String, rawPath: String, code: Int = 200) {
fun stubUrl(url: String, rawPath: String, code: Int = 200, extension: String = ".json") {
val iStream =
InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath.json")
InstrumentationRegistry.getInstrumentation().context.assets.open("$rawPath$extension")
val data = iStream.bufferedReader().use(BufferedReader::readText)
mockingNetworkInterceptor.addUrlStub(url = url, data = data, code = code)
}

View File

@@ -6,10 +6,10 @@ import androidx.test.filters.SmallTest
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.furtherInfoScreen
import com.appttude.h_mal.monoWeather.robot.settingsScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Ignore
import org.junit.Test
import tools.fastlane.screengrab.Screengrab
@@ -18,8 +18,8 @@ import tools.fastlane.screengrab.Screengrab
class SnapshotCaptureTest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubLocation("London", 51.51, -0.13)
stubEndpoint(baseUrl, Stubs.Metric)
stubLocation("London", 51.5064, -0.12721)
clearPrefs()
}
@@ -52,7 +52,7 @@ class SnapshotCaptureTest : BaseTest<MainActivity>(MainActivity::class.java) {
openMenuItem()
}
settingsScreen {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Imperial)
stubEndpoint(baseUrl, Stubs.Imperial)
Screengrab.screenshot("SettingsScreen")
}
}

View File

@@ -13,6 +13,7 @@ import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.types.UnitType.Companion.getLabel
fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() }
@@ -24,10 +25,7 @@ class SettingsScreen : BaseTestRobot() {
RecyclerViewActions.actionOnItem<ViewHolder>(
ViewMatchers.hasDescendant(withText(R.string.weather_units)),
click()))
val label = when (unitType) {
UnitType.METRIC -> "Metric"
UnitType.IMPERIAL -> "Imperial"
}
val label = unitType.getLabel()
onView(withText(label))
.inRoot(isDialog())

View File

@@ -3,7 +3,6 @@ package com.appttude.h_mal.monoWeather.robot
import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.monoWeather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily
import com.appttude.h_mal.monoWeather.ui.home.adapter.further.ViewHolderFurtherDetails
fun weatherScreen(func: WeatherScreen.() -> Unit) = WeatherScreen().apply { func() }
class WeatherScreen : BaseTestRobot() {

View File

@@ -4,35 +4,30 @@ package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Ignore
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runners.MethodSorters
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class HomePageNoDataUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.InvalidKey, 400)
stubEndpoint(baseUrl, Stubs.InvalidKey, 400, ".txt")
}
@Test
fun loadApp_invalidKeyWeatherResponse_returnsEmptyViewPage() {
weatherScreen {
// verify empty
verifyUnableToRetrieve()
}
}
@Ignore("Test is flakey - must investigate")
@Test
fun invalidKeyWeatherResponse_swipeToRefresh_returnsValidPage() {
weatherScreen {
// verify empty
verifyUnableToRetrieve()
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubEndpoint(baseUrl, Stubs.Metric)
refresh()
verifyCurrentTemperature(2)
verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location")
}
}
}

View File

@@ -2,20 +2,17 @@ package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.furtherInfoScreen
import com.appttude.h_mal.monoWeather.robot.settingsScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen
import okio.IOException
import org.junit.Test
import tools.fastlane.screengrab.Screengrab
class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubEndpoint(baseUrl, Stubs.Metric)
clearPrefs()
}
@@ -23,7 +20,7 @@ class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
fun loadApp_validWeatherResponse_returnsValidPage() {
weatherScreen {
isDisplayed()
verifyCurrentTemperature(2)
verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location")
}
}
@@ -32,35 +29,15 @@ class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
fun loadApp_validWeatherResponse_viewFurtherDetailsPage() {
weatherScreen {
isDisplayed()
verifyCurrentTemperature(2)
verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location")
tapDayInformationByPosition(4)
}
furtherInfoScreen {
isDisplayed()
verifyMaxTemperature(12)
verifyAverageTemperature(9)
verifyMaxTemperature(15)
verifyAverageTemperature(11)
}
}
@Test
fun loadApp_changeToImperial_returnsValidPage() {
weatherScreen {
isDisplayed()
verifyCurrentTemperature(2)
verifyCurrentLocation("Mock Location")
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Imperial)
openMenuItem()
}
settingsScreen {
selectWeatherUnits(UnitType.IMPERIAL)
goBack()
}
weatherScreen {
isDisplayed()
refresh()
verifyCurrentTemperature(58)
verifyCurrentLocation("Mock Location")
}
}
}

View File

@@ -0,0 +1,40 @@
package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.settingsScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Test
class SettingsPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint(baseUrl, Stubs.Metric)
clearPrefs()
}
@Test
fun loadApp_changeToImperial_returnsValidPage() {
weatherScreen {
isDisplayed()
verifyCurrentTemperature(13)
verifyCurrentLocation("Mock Location")
stubEndpoint(baseUrl, Stubs.Imperial)
openMenuItem()
}
settingsScreen {
selectWeatherUnits(UnitType.IMPERIAL)
goBack()
}
weatherScreen {
isDisplayed()
refresh()
verifyCurrentTemperature(56)
verifyCurrentLocation("Mock Location")
}
}
}

View File

@@ -4,6 +4,7 @@ package com.appttude.h_mal.monoWeather.tests
import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.ContainerRobot.Tab.WORLD
import com.appttude.h_mal.monoWeather.robot.addLocation
import com.appttude.h_mal.monoWeather.robot.container
@@ -14,7 +15,8 @@ import org.junit.Test
class WorldPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
stubEndpoint(baseUrl, Stubs.Metric)
stubLocation("London", 51.5064,-0.12721)
}
@Test
@@ -26,8 +28,8 @@ class WorldPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
clickFab()
}
addLocation {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Sydney)
stubLocation("Sydney", -33.89, -151.12)
stubEndpoint(baseUrl, Stubs.Sydney)
stubLocation("Sydney",-33.8696,151.207)
setLocation("Sydney")
submit()
}
@@ -36,7 +38,7 @@ class WorldPageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
}
weatherScreen {
isDisplayed()
verifyCurrentTemperature(12)
verifyCurrentTemperature(16)
verifyCurrentLocation("Sydney")
}
}

View File

@@ -3,6 +3,7 @@ package com.appttude.h_mal.atlas_weather.application
import android.app.Application
import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.network.Api
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
@@ -12,7 +13,6 @@ import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.google.gson.Gson
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.KodeinContainer
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
@@ -29,7 +29,7 @@ abstract class BaseAppClass : Application(), KodeinAware {
val parentModule = Kodein.Module("Parent Module", allowSilentOverride = true) {
import(androidXModule(this@BaseAppClass))
bind() from singleton { createNetworkModule() }
bind() from singleton { createNetworkModule() as WeatherApi }
bind() from singleton { createLocationModule() }
bind() from singleton { Gson() }
@@ -41,7 +41,7 @@ abstract class BaseAppClass : Application(), KodeinAware {
bind() from singleton { WeatherSource(instance(), instance()) }
}
abstract fun createNetworkModule(): WeatherApi
abstract fun createNetworkModule(): Api
abstract fun createLocationModule(): LocationProvider
abstract fun createRoomDatabase(): AppDatabase

View File

@@ -5,8 +5,8 @@ import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.LocationType
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.utils.getSymbol
import java.io.IOException
class WeatherSource(
@@ -33,12 +33,14 @@ class WeatherSource(
}
@Throws(IOException::class)
suspend fun forceFetchWeather(latLon: Pair<Double, Double>,
locationType: LocationType = LocationType.Town): FullWeather {
suspend fun forceFetchWeather(
latLon: Pair<Double, Double>,
locationType: LocationType = LocationType.Town
): FullWeather {
// get data from database
val weatherEntity = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
// check unit type - if same do nothing
val units = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F"
val units = repository.getUnitType().getSymbol()
if (weatherEntity.weather.temperatureUnit == units) return weatherEntity.weather
// load data for forced
return fetchWeather(
@@ -55,11 +57,13 @@ class WeatherSource(
// Get weather from api
val weather = repository
.getWeatherFromApi(latLon.first.toString(), latLon.second.toString())
val lat = weather.latitude ?: latLon.first
val long = weather.longitude ?: latLon.second
val currentLocation =
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, locationType)
val unit = repository.getUnitType()
val fullWeather = FullWeather(weather).apply {
temperatureUnit = if (unit == UnitType.METRIC) "°C" else "°F"
locationProvider.getLocationNameFromLatLong(lat, long, locationType)
val unit = repository.getUnitType().getSymbol()
val fullWeather = weather.mapData().apply {
temperatureUnit = unit
locationString = currentLocation
}
val entityItem = EntityItem(locationName, fullWeather)

View File

@@ -1,6 +1,6 @@
package com.appttude.h_mal.atlas_weather.data.network
class NetworkModule : BaseNetworkModule() {
override fun baseUrl(): String = "https://api.openweathermap.org/data/2.5/"
override fun baseUrl(): String = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/"
}

View File

@@ -1,9 +1,7 @@
package com.appttude.h_mal.atlas_weather.data.network
import org.json.JSONException
import org.json.JSONObject
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
abstract class ResponseUnwrap {
@@ -15,18 +13,7 @@ abstract class ResponseUnwrap {
if (response.isSuccessful) {
return response.body()!!
} else {
val error = response.errorBody()?.string()
val errorMessage = error?.let {
try {
JSONObject(it).getString("message")
} catch (e: JSONException) {
e.printStackTrace()
null
}
} ?: "Error Code: ${response.code()}"
throw IOException(errorMessage)
throw HttpException(response)
}
}
}

View File

@@ -1,20 +1,20 @@
package com.appttude.h_mal.atlas_weather.data.network
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface WeatherApi : Api {
@GET("onecall?")
@GET("{location}")
suspend fun getFromApi(
@Query("lat") query: String,
@Query("lon") lon: String,
@Query("exclude") exclude: String = "minutely",
@Query("units") units: String = "metric"
): Response<WeatherResponse>
@Path("location") location: String,
@Query("contentType") exclude: String = "json",
@Query("unitGroup") units: String = "metric"
): Response<WeatherApiResponse>
}

View File

@@ -16,7 +16,7 @@ class QueryParamsInterceptor : Interceptor {
val original = chain.request()
val url = original.url.newBuilder()
.addQueryParameter("appid", id)
.addQueryParameter("key", id)
.build()
// Request customization: add request headers

View File

@@ -1,11 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.network.networkUtils
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor
import com.google.gson.GsonBuilder
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.lang.reflect.Modifier
import java.util.concurrent.TimeUnit
val loggingInterceptor = HttpLoggingInterceptor().apply {
@@ -34,6 +36,13 @@ fun buildOkHttpClient(
return builder.build()
}
fun createGsonConverterFactory(): GsonConverterFactory {
val gson = GsonBuilder()
.excludeFieldsWithModifiers(Modifier.TRANSIENT)
.create()
return GsonConverterFactory.create(gson)
}
fun <T> createRetrofit(
baseUrl: String,
okHttpClient: OkHttpClient,
@@ -42,7 +51,7 @@ fun <T> createRetrofit(
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(createGsonConverterFactory())
.build()
.create(service)
}

View File

@@ -1,48 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Current(
@field:SerializedName("sunrise")
val sunrise: Int? = null,
@field:SerializedName("temp")
val temp: Double? = null,
@field:SerializedName("visibility")
val visibility: Int? = null,
@field:SerializedName("uvi")
val uvi: Double? = null,
@field:SerializedName("pressure")
val pressure: Int? = null,
@field:SerializedName("clouds")
val clouds: Int? = null,
@field:SerializedName("feels_like")
val feelsLike: Double? = null,
@field:SerializedName("dt")
val dt: Int? = null,
@field:SerializedName("wind_deg")
val windDeg: Int? = null,
@field:SerializedName("dew_point")
val dewPoint: Double? = null,
@field:SerializedName("sunset")
val sunset: Int? = null,
@field:SerializedName("weather")
val weather: List<WeatherItem?>? = null,
@field:SerializedName("humidity")
val humidity: Int? = null,
@field:SerializedName("wind_speed")
val windSpeed: Double? = null
)

View File

@@ -1,51 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class DailyItem(
@field:SerializedName("sunrise")
val sunrise: Int? = null,
@field:SerializedName("temp")
val temp: Temp? = null,
@field:SerializedName("uvi")
val uvi: Double? = null,
@field:SerializedName("pressure")
val pressure: Int? = null,
@field:SerializedName("clouds")
val clouds: Int? = null,
@field:SerializedName("feels_like")
val feelsLike: FeelsLike? = null,
@field:SerializedName("dt")
val dt: Int? = null,
@field:SerializedName("pop")
val pop: Double? = null,
@field:SerializedName("wind_deg")
val windDeg: Int? = null,
@field:SerializedName("dew_point")
val dewPoint: Double? = null,
@field:SerializedName("sunset")
val sunset: Int? = null,
@field:SerializedName("weather")
val weather: List<WeatherItem?>? = null,
@field:SerializedName("humidity")
val humidity: Int? = null,
@field:SerializedName("wind_speed")
val windSpeed: Double? = null,
@field:SerializedName("rain")
val rain: Double? = null
)

View File

@@ -1,18 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class FeelsLike(
@field:SerializedName("eve")
val eve: Double? = null,
@field:SerializedName("night")
val night: Double? = null,
@field:SerializedName("day")
val day: Double? = null,
@field:SerializedName("morn")
val morn: Double? = null
)

View File

@@ -1,48 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Hour(
@field:SerializedName("sunrise")
val sunrise: Int? = null,
@field:SerializedName("temp")
val temp: Double? = null,
@field:SerializedName("visibility")
val visibility: Int? = null,
@field:SerializedName("uvi")
val uvi: Double? = null,
@field:SerializedName("pressure")
val pressure: Int? = null,
@field:SerializedName("clouds")
val clouds: Int? = null,
@field:SerializedName("feels_like")
val feelsLike: Double? = null,
@field:SerializedName("dt")
val dt: Int? = null,
@field:SerializedName("wind_deg")
val windDeg: Int? = null,
@field:SerializedName("dew_point")
val dewPoint: Double? = null,
@field:SerializedName("sunset")
val sunset: Int? = null,
@field:SerializedName("weather")
val weather: List<WeatherItem?>? = null,
@field:SerializedName("humidity")
val humidity: Int? = null,
@field:SerializedName("wind_speed")
val windSpeed: Double? = null
)

View File

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Response(
@field:SerializedName("current")
val current: Current? = null,
@field:SerializedName("timezone")
val timezone: String? = null,
@field:SerializedName("timezone_offset")
val timezoneOffset: Int? = null,
@field:SerializedName("daily")
val daily: List<DailyItem?>? = null,
@field:SerializedName("lon")
val lon: Double? = null,
@field:SerializedName("lat")
val lat: Double? = null
)

View File

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class Temp(
@field:SerializedName("min")
val min: Double? = null,
@field:SerializedName("max")
val max: Double? = null,
@field:SerializedName("eve")
val eve: Double? = null,
@field:SerializedName("night")
val night: Double? = null,
@field:SerializedName("day")
val day: Double? = null,
@field:SerializedName("morn")
val morn: Double? = null
)

View File

@@ -1,18 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class WeatherItem(
@field:SerializedName("icon")
val icon: String? = null,
@field:SerializedName("description")
val description: String? = null,
@field:SerializedName("main")
val main: String? = null,
@field:SerializedName("id")
val id: Int? = null
)

View File

@@ -1,28 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.response.forecast
import com.google.gson.annotations.SerializedName
data class WeatherResponse(
@field:SerializedName("current")
val current: Current? = null,
@field:SerializedName("timezone")
val timezone: String? = null,
@field:SerializedName("timezone_offset")
val timezoneOffset: Int? = null,
@field:SerializedName("hourly")
val hourly: List<Hour>? = null,
@field:SerializedName("daily")
val daily: List<DailyItem>? = null,
@field:SerializedName("lon")
val lon: Double = 0.00,
@field:SerializedName("lat")
val lat: Double = 0.00
)

View File

@@ -0,0 +1,16 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class Alerts(
@SerializedName("event") var event: String? = null,
@SerializedName("headline") var headline: String? = null,
@SerializedName("ends") var ends: String? = null,
@SerializedName("endsEpoch") var endsEpoch: Int? = null,
@SerializedName("onset") var onset: String? = null,
@SerializedName("onsetEpoch") var onsetEpoch: Int? = null,
@SerializedName("id") var id: String? = null,
@SerializedName("language") var language: String? = null,
@SerializedName("link") var link: String? = null,
@SerializedName("description") var description: String? = null,
)

View File

@@ -0,0 +1,36 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class CurrentConditions(
@SerializedName("datetime") var datetime: String? = null,
@SerializedName("datetimeEpoch") var datetimeEpoch: Int? = null,
@SerializedName("temp") var temp: Double? = null,
@SerializedName("feelslike") var feelslike: Double? = null,
@SerializedName("humidity") var humidity: Double? = null,
@SerializedName("dew") var dew: Double? = null,
@SerializedName("precip") var precip: Double? = null,
@SerializedName("precipprob") var precipprob: Double? = null,
@SerializedName("snow") var snow: Int? = null,
@SerializedName("snowdepth") var snowdepth: Int? = null,
@SerializedName("preciptype") var preciptype: ArrayList<String> = arrayListOf(),
@SerializedName("windgust") var windgust: Double? = null,
@SerializedName("windspeed") var windspeed: Double? = null,
@SerializedName("winddir") var winddir: Double? = null,
@SerializedName("pressure") var pressure: Double? = null,
@SerializedName("visibility") var visibility: Double? = null,
@SerializedName("cloudcover") var cloudcover: Double? = null,
@SerializedName("solarradiation") var solarradiation: Double? = null,
@SerializedName("solarenergy") var solarenergy: Double? = null,
@SerializedName("uvindex") var uvindex: Int? = null,
@SerializedName("conditions") var conditions: String? = null,
@SerializedName("icon") var icon: String? = null,
@SerializedName("stations") var stations: ArrayList<String> = arrayListOf(),
@SerializedName("source") var source: String? = null,
@SerializedName("sunrise") var sunrise: String? = null,
@SerializedName("sunriseEpoch") var sunriseEpoch: Int? = null,
@SerializedName("sunset") var sunset: String? = null,
@SerializedName("sunsetEpoch") var sunsetEpoch: Int? = null,
@SerializedName("moonphase") var moonphase: Double? = null
)

View File

@@ -0,0 +1,45 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class Days(
@SerializedName("datetime") var datetime: String? = null,
@SerializedName("datetimeEpoch") var datetimeEpoch: Int? = null,
@SerializedName("tempmax") var tempmax: Double? = null,
@SerializedName("tempmin") var tempmin: Double? = null,
@SerializedName("temp") var temp: Double? = null,
@SerializedName("feelslikemax") var feelslikemax: Double? = null,
@SerializedName("feelslikemin") var feelslikemin: Double? = null,
@SerializedName("feelslike") var feelslike: Double? = null,
@SerializedName("dew") var dew: Double? = null,
@SerializedName("humidity") var humidity: Double? = null,
@SerializedName("precip") var precip: Number? = null,
@SerializedName("precipprob") var precipprob: Double? = null,
@SerializedName("precipcover") var precipcover: Double? = null,
@SerializedName("preciptype") var preciptype: ArrayList<String> = arrayListOf(),
@SerializedName("snow") var snow: Int? = null,
@SerializedName("snowdepth") var snowdepth: Int? = null,
@SerializedName("windgust") var windgust: Double? = null,
@SerializedName("windspeed") var windspeed: Double? = null,
@SerializedName("winddir") var winddir: Double? = null,
@SerializedName("pressure") var pressure: Double? = null,
@SerializedName("cloudcover") var cloudcover: Double? = null,
@SerializedName("visibility") var visibility: Double? = null,
@SerializedName("solarradiation") var solarradiation: Double? = null,
@SerializedName("solarenergy") var solarenergy: Double? = null,
@SerializedName("uvindex") var uvindex: Int? = null,
@SerializedName("severerisk") var severerisk: Int? = null,
@SerializedName("sunrise") var sunrise: String? = null,
@SerializedName("sunriseEpoch") var sunriseEpoch: Int? = null,
@SerializedName("sunset") var sunset: String? = null,
@SerializedName("sunsetEpoch") var sunsetEpoch: Int? = null,
@SerializedName("moonphase") var moonphase: Double? = null,
@SerializedName("conditions") var conditions: String? = null,
@SerializedName("description") var description: String? = null,
@SerializedName("icon") var icon: String? = null,
@SerializedName("stations") var stations: ArrayList<String> = arrayListOf(),
@SerializedName("source") var source: String? = null,
@SerializedName("hours") var hours: ArrayList<Hours> = arrayListOf()
)

View File

@@ -0,0 +1,32 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.google.gson.annotations.SerializedName
data class Hours(
@SerializedName("datetime") var datetime: String? = null,
@SerializedName("datetimeEpoch") var datetimeEpoch: Int? = null,
@SerializedName("temp") var temp: Double? = null,
@SerializedName("feelslike") var feelslike: Double? = null,
@SerializedName("humidity") var humidity: Double? = null,
@SerializedName("dew") var dew: Double? = null,
@SerializedName("precip") var precip: Number? = null,
@SerializedName("precipprob") var precipprob: Double? = null,
@SerializedName("snow") var snow: Int? = null,
@SerializedName("snowdepth") var snowdepth: Int? = null,
@SerializedName("preciptype") var preciptype: ArrayList<String> = arrayListOf(),
@SerializedName("windgust") var windgust: Double? = null,
@SerializedName("windspeed") var windspeed: Double? = null,
@SerializedName("winddir") var winddir: Double? = null,
@SerializedName("pressure") var pressure: Double? = null,
@SerializedName("visibility") var visibility: Double? = null,
@SerializedName("cloudcover") var cloudcover: Double? = null,
@SerializedName("solarradiation") var solarradiation: Double? = null,
@SerializedName("solarenergy") var solarenergy: Double? = null,
@SerializedName("uvindex") var uvindex: Int? = null,
@SerializedName("severerisk") var severerisk: Int? = null,
@SerializedName("conditions") var conditions: String? = null,
@SerializedName("icon") var icon: String? = null,
@SerializedName("stations") var stations: ArrayList<String> = arrayListOf(),
@SerializedName("source") var source: String? = null
)

View File

@@ -0,0 +1,40 @@
package com.appttude.h_mal.atlas_weather.data.network.response.weather
import com.appttude.h_mal.atlas_weather.model.DataMapper
import com.appttude.h_mal.atlas_weather.model.weather.Current
import com.appttude.h_mal.atlas_weather.model.weather.DailyWeather
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.google.gson.annotations.SerializedName
import com.appttude.h_mal.atlas_weather.model.weather.Hour as FullWeatherHour
data class WeatherApiResponse(
@SerializedName("queryCost") var queryCost: Int? = null,
@SerializedName("latitude") var latitude: Double? = null,
@SerializedName("longitude") var longitude: Double? = null,
@SerializedName("resolvedAddress") var resolvedAddress: String? = null,
@SerializedName("address") var address: String? = null,
@SerializedName("timezone") var timezone: String? = null,
@SerializedName("tzoffset") var tzoffset: Int? = null,
@SerializedName("description") var description: String? = null,
@SerializedName("days") var days: ArrayList<Days> = arrayListOf(),
@SerializedName("alerts") var alerts: ArrayList<Alerts> = arrayListOf(),
@SerializedName("currentConditions") var currentConditions: CurrentConditions? = CurrentConditions()
): DataMapper<FullWeather> {
override fun mapData(): FullWeather {
val hours = mutableListOf(days[0].hours).apply { add(days[1].hours) }.flatten().subList(0,23).map { FullWeatherHour(it) }.toList()
val collectedDays = mutableListOf(days.subList(0,7)).flatten().map { DailyWeather(it) }.toList()
return FullWeather(
current = Current(currentConditions),
timezone = timezone,
timezoneOffset = tzoffset,
hourly = hours,
daily = collectedDays,
lat = latitude,
lon = longitude,
locationString = address,
temperatureUnit = null
)
}
}

View File

@@ -1,13 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.repository
import androidx.lifecycle.LiveData
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.UnitType
interface Repository {
suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse
suspend fun getWeatherFromApi(lat: String, long: String): WeatherApiResponse
suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem)
suspend fun saveWeatherListToRoom(list: List<EntityItem>)
fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>>

View File

@@ -2,7 +2,7 @@ package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.data.network.ResponseUnwrap
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
@@ -20,8 +20,9 @@ class RepositoryImpl(
override suspend fun getWeatherFromApi(
lat: String,
long: String
): WeatherResponse {
return responseUnwrap { api.getFromApi(lat, long, units = prefs.getUnitsType().name.lowercase()) }
): WeatherApiResponse {
val unit = if (prefs.getUnitsType() == UnitType.METRIC) "metric" else "us"
return responseUnwrap { api.getFromApi(location = "$lat,$long", units = unit) }
}
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {

View File

@@ -11,14 +11,13 @@ import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetError
import com.appttude.h_mal.atlas_weather.model.widget.WidgetState
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.utils.getSymbol
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
@@ -45,9 +44,11 @@ class ServicesHelper(
// Get weather from api
val weather = repository
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
val lat = weather.latitude ?: latLong.first
val long = weather.longitude ?: latLong.second
val currentLocation =
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
val fullWeather = FullWeather(weather).apply {
locationProvider.getLocationNameFromLatLong(lat, long)
val fullWeather = weather.mapData().apply {
temperatureUnit = "°C"
locationString = currentLocation
}
@@ -105,8 +106,11 @@ class ServicesHelper(
return WidgetState.HasError(error)
}
val lat = weather.latitude ?: latLong.first
val long = weather.longitude ?: latLong.second
val currentLocation = try {
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
locationProvider.getLocationNameFromLatLong(lat, long)
} catch (e: IOException) {
val data = getWidgetWeatherCollection()
data?.let {
@@ -120,8 +124,8 @@ class ServicesHelper(
return WidgetState.HasError(error)
}
val fullWeather = FullWeather(weather).apply {
temperatureUnit = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F"
val fullWeather = weather.mapData().apply {
temperatureUnit = repository.getUnitType().getSymbol()
locationString = currentLocation
}
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
@@ -140,7 +144,7 @@ class ServicesHelper(
result.weather.let {
val bitmap = it.current?.icon
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
val location = locationProvider.getLocationNameFromLatLong(it.lat!!, it.lon!!)
val temp = it.current?.temp?.toInt().toString()
WidgetData(location, bitmap, temp, epoc)
@@ -177,7 +181,7 @@ class ServicesHelper(
val widgetData = result.weather.let {
val bitmap = it.current?.icon
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
val location = locationProvider.getLocationNameFromLatLong(it.lat!!, it.lon!!)
val temp = it.current?.temp?.toInt().toString()
val epoc = System.currentTimeMillis()
@@ -186,7 +190,7 @@ class ServicesHelper(
val list = mutableListOf<InnerWidgetCellData>()
result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather ->
result.weather.daily?.drop(1)?.dropLast(1)?.forEach { dailyWeather ->
val day = dailyWeather.dt?.toSmallDayName()
val icon = dailyWeather.icon
val temp = dailyWeather.max?.toInt().toString()
@@ -216,7 +220,7 @@ class ServicesHelper(
val list = mutableListOf<InnerWidgetCellData>()
result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather ->
result.weather.daily?.drop(1)?.dropLast(1)?.forEach { dailyWeather ->
val day = dailyWeather.dt?.toSmallDayName()
val icon = dailyWeather.icon
val temp = dailyWeather.max?.toInt().toString()

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.atlas_weather.model
interface DataMapper <T: Any> {
fun mapData(): T
}

View File

@@ -0,0 +1,28 @@
package com.appttude.h_mal.atlas_weather.model
enum class IconMapper(val label: String) {
snow("13d"),
snow_showers_day("13d"),
snow_showers_night("13n"),
thunder_rain("11d"),
thunder_showers_day("11d"),
thunder_showers_night("11n"),
rain("10d"),
showers_day("10d"),
showers_night("10n"),
fog("50d"),
wind("50d"),
cloudy("04d"),
partly_cloudy_day("03d"),
partly_cloudy_night("03n"),
clear_day("01d"),
clear_night("01n");
companion object{
fun findIconCode(iconId: String?): String? {
val label = iconId?.replace("-", "_")
val enumName = IconMapper.entries.find { it.name == label }
return enumName?.label
}
}
}

View File

@@ -40,8 +40,7 @@ data class Forecast(
parcel.readString(),
parcel.readString(),
parcel.readString()
) {
}
)
constructor(dailyWeather: DailyWeather) : this(
dailyWeather.dt?.toDayString(),

View File

@@ -10,7 +10,7 @@ data class WeatherDisplay(
val averageTemp: Double?,
var unit: String?,
var location: String?,
val iconURL: String?,
var iconURL: String?,
val description: String?,
val hourly: List<Hour>?,
val forecast: List<Forecast>?,
@@ -40,8 +40,7 @@ data class WeatherDisplay(
parcel.readDouble(),
parcel.readDouble(),
parcel.readString()
) {
}
)
constructor(entity: EntityItem) : this(
entity.weather.current?.temp,
@@ -56,8 +55,8 @@ data class WeatherDisplay(
entity.weather.daily?.get(0)?.pop?.times(100)?.toInt()?.toString(),
entity.weather.current?.humidity?.toString(),
entity.weather.current?.clouds?.toString(),
entity.weather.lat,
entity.weather.lon,
entity.weather.lat!!,
entity.weather.lon!!,
entity.weather.locationString
)

View File

@@ -8,11 +8,15 @@ enum class UnitType {
companion object {
fun getByName(name: String?): UnitType? {
return values().firstOrNull {
return entries.firstOrNull {
it.name.lowercase(Locale.ROOT) == name?.lowercase(
Locale.ROOT
)
}
}
fun UnitType.getLabel() = name.lowercase().replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
}
}

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current
import com.appttude.h_mal.atlas_weather.data.network.response.weather.CurrentConditions
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
data class Current(
@@ -23,23 +24,23 @@ data class Current(
val windSpeed: Double? = null
) {
constructor(dailyItem: Current) : this(
dailyItem.dt,
dailyItem.sunrise,
dailyItem.sunset,
dailyItem.temp,
dailyItem.visibility,
dailyItem.uvi,
dailyItem.pressure,
dailyItem.clouds,
dailyItem.feelsLike,
dailyItem.windDeg,
dailyItem.dewPoint,
generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon),
dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id,
dailyItem.humidity,
dailyItem.windSpeed
constructor(currentConditions: CurrentConditions?) : this(
dt = currentConditions?.datetimeEpoch,
sunrise = currentConditions?.sunriseEpoch,
sunset = currentConditions?.sunsetEpoch,
temp = currentConditions?.temp,
visibility = currentConditions?.visibility?.toInt(),
uvi = currentConditions?.uvindex?.toDouble(),
pressure = currentConditions?.pressure?.toInt(),
clouds = currentConditions?.cloudcover?.toInt(),
feelsLike = currentConditions?.feelslike,
windDeg = currentConditions?.winddir?.toInt(),
dewPoint = currentConditions?.dew,
icon = generateIconUrlString(IconMapper.findIconCode(currentConditions?.icon)),
description = currentConditions?.conditions,
main = currentConditions?.conditions,
id = currentConditions?.datetimeEpoch,
humidity = currentConditions?.humidity?.toInt(),
windSpeed = currentConditions?.windspeed
)
}

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem
import com.appttude.h_mal.atlas_weather.data.network.response.weather.Days
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
@@ -27,28 +28,29 @@ data class DailyWeather(
val rain: Double?
) {
constructor(dailyItem: DailyItem) : this(
dailyItem.dt,
dailyItem.sunrise,
dailyItem.sunset,
dailyItem.temp?.min,
dailyItem.temp?.max,
dailyItem.temp?.day,
dailyItem.feelsLike?.day,
dailyItem.pressure,
dailyItem.humidity,
dailyItem.dewPoint,
dailyItem.windSpeed,
dailyItem.windDeg,
generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon),
dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id,
dailyItem.clouds,
dailyItem.pop,
dailyItem.uvi,
dailyItem.rain
constructor(days: Days) : this(
days.datetimeEpoch,
days.sunriseEpoch,
days.sunsetEpoch,
days.tempmin,
days.tempmax,
days.temp,
days.feelslike,
days.pressure?.toInt(),
days.humidity?.toInt(),
days.dew,
days.windspeed,
days.winddir?.toInt(),
generateIconUrlString(
IconMapper.findIconCode(days.icon)
),
days.description,
days.conditions,
days.datetimeEpoch,
days.cloudcover?.toInt(),
days.precipprob,
days.uvindex?.toDouble(),
days.precip?.toDouble()
)
}

View File

@@ -1,29 +1,15 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
data class FullWeather(
val current: Current? = null,
val timezone: String? = null,
val timezoneOffset: Int? = null,
val hourly: List<Hour>? = null,
val daily: List<DailyWeather>? = null,
val lon: Double = 0.00,
val lat: Double = 0.00,
val lon: Double? = null,
val lat: Double? = null,
var locationString: String? = null,
var temperatureUnit: String? = null
) {
constructor(weatherResponse: WeatherResponse) : this(
weatherResponse.current?.let { Current(it) },
weatherResponse.timezone,
weatherResponse.timezoneOffset,
weatherResponse.hourly?.subList(0, 23)?.map { Hour(it) },
weatherResponse.daily?.map { DailyWeather(it) },
weatherResponse.lon,
weatherResponse.lat
)
}
)

View File

@@ -2,8 +2,9 @@ package com.appttude.h_mal.atlas_weather.model.weather
import android.os.Parcel
import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour
import com.appttude.h_mal.atlas_weather.data.network.response.weather.Hours as WeatherHour
data class Hour(
@@ -16,13 +17,13 @@ data class Hour(
parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Double::class.java.classLoader) as? Double,
parcel.readString()
) {
}
)
constructor(hour: ForecastHour) : this(
hour.dt,
hour.temp,
generateIconUrlString(hour.weather?.getOrNull(0)?.icon)
constructor(weatherHour: WeatherHour) : this(
weatherHour.datetimeEpoch,
weatherHour.temp,
generateIconUrlString(IconMapper.findIconCode(weatherHour.icon))
)
override fun writeToParcel(parcel: Parcel, flags: Int) {

View File

@@ -1,5 +1,7 @@
package com.appttude.h_mal.atlas_weather.utils
import com.appttude.h_mal.atlas_weather.model.types.UnitType
fun generateIconUrlString(icon: String?): String? {
return icon?.let {
@@ -9,4 +11,6 @@ fun generateIconUrlString(icon: String?): String? {
.append("@2x.png")
.toString()
}
}
}
fun UnitType.getSymbol(): String = if (this == UnitType.METRIC) "°C" else "°F"

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.data
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
@@ -18,6 +18,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import java.io.IOException
import kotlin.properties.Delegates
import kotlin.test.assertEquals
class WeatherSourceTest : BaseTest() {
@@ -31,41 +32,43 @@ class WeatherSourceTest : BaseTest() {
@MockK
lateinit var locationProvider: LocationProviderImpl
private lateinit var weatherResponse: WeatherResponse
private lateinit var weatherResponse: WeatherApiResponse
private var lat by Delegates.notNull<Double>()
private var long by Delegates.notNull<Double>()
private lateinit var latlon: Pair<Double, Double>
private lateinit var fullWeather: FullWeather
@Before
fun setUp() {
MockKAnnotations.init(this)
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
weatherResponse = getTestData("new_response.json", WeatherApiResponse::class.java)
lat = weatherResponse.latitude!!
long = weatherResponse.longitude!!
latlon = Pair(lat, long)
fullWeather = weatherResponse.mapData().apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
}
@Test
fun fetchDataForSingleLocation_validLocation_validReturn() {
// Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
val fullWeather = FullWeather(weatherResponse).apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Act
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
coEvery {
repository.getWeatherFromApi(
weatherResponse.lat.toString(),
weatherResponse.lon.toString()
lat.toString(),
long.toString()
)
}.returns(weatherResponse)
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon,
lat,
long,
LocationType.City
)
}.returns(CURRENT_LOCATION)
@@ -82,17 +85,13 @@ class WeatherSourceTest : BaseTest() {
@Test(expected = IOException::class)
fun fetchDataForSingleLocation_failedWeatherApi_invalidReturn() {
// Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
// Act
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery {
repository.getWeatherFromApi(
weatherResponse.lat.toString(),
weatherResponse.lon.toString()
lat.toString(),
long.toString()
)
} throws IOException("Unable fetch data")
@@ -103,23 +102,19 @@ class WeatherSourceTest : BaseTest() {
@Test(expected = IOException::class)
fun fetchDataForSingleLocation_failedLocation_invalidReturn() {
// Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
// Act
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery {
repository.getWeatherFromApi(
weatherResponse.lat.toString(),
weatherResponse.lon.toString()
lat.toString(),
long.toString()
)
} returns weatherResponse
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon
lat,
long
)
}.throws(IOException())
@@ -130,14 +125,6 @@ class WeatherSourceTest : BaseTest() {
@Test
fun searchAboveFallbackTime_validLocation_validReturn() {
// Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
val fullWeather = FullWeather(weatherResponse).apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Act
@@ -153,14 +140,6 @@ class WeatherSourceTest : BaseTest() {
@Test
fun forceFetchDataForSingleLocation_validLocation_validReturn() {
// Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
val fullWeather = FullWeather(weatherResponse).apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Act
@@ -169,14 +148,14 @@ class WeatherSourceTest : BaseTest() {
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
coEvery {
repository.getWeatherFromApi(
weatherResponse.lat.toString(),
weatherResponse.lon.toString()
weatherResponse.latitude.toString(),
weatherResponse.longitude.toString()
)
}.returns(weatherResponse)
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon,
lat,
long,
LocationType.City
)
}.returns(CURRENT_LOCATION)

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
@@ -17,7 +17,9 @@ import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import java.io.IOException
import org.mockito.ArgumentMatchers.anyDouble
import org.mockito.ArgumentMatchers.anyString
import retrofit2.HttpException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
@@ -29,8 +31,7 @@ class RepositoryImplTest : BaseTest() {
lateinit var repository: RepositoryImpl
@MockK
lateinit var api: WeatherApi
@MockK lateinit var api: WeatherApi
@MockK
lateinit var db: AppDatabase
@@ -86,37 +87,44 @@ class RepositoryImplTest : BaseTest() {
@Test
fun getWeatherFromApi_validLatLong_validSearch() {
//Arrange
val mockResponse = createSuccessfulRetrofitMock<WeatherResponse>()
val lat = anyDouble().toString()
val long = anyDouble().toString()
val mockResponse = createSuccessfulRetrofitMock<WeatherApiResponse>()
//Act
//create a successful retrofit response
every { prefs.getUnitsType() } returns (UnitType.METRIC)
coEvery { api.getFromApi("", "") }.returns(mockResponse)
coEvery { api.getFromApi(location = "$lat,$long") }.returns(mockResponse)
// Assert
runBlocking {
val result = repository.getWeatherFromApi("", "")
assertIs<WeatherResponse>(result)
val result = repository.getWeatherFromApi(lat, long)
assertIs<WeatherApiResponse>(result)
}
}
@Test
fun getWeatherFromApi_validLatLong_invalidResponse() {
//Arrange
val mockResponse = createErrorRetrofitMock<WeatherResponse>()
val errorMessage = "Why dont you have a valid api key?"
val mockResponse = createErrorRetrofitMock<WeatherApiResponse>(errorMessage)
val lat = anyString()
val long = anyString()
//Act
//create a successful retrofit response
every { prefs.getUnitsType() } returns (UnitType.METRIC)
coEvery { api.getFromApi(any(), any()) } returns (mockResponse)
coEvery { api.getFromApi(location = "$lat,$long") } returns (mockResponse)
// Assert
val ioExceptionReturned = assertFailsWith<IOException> {
val ioExceptionReturned = assertFailsWith<HttpException> {
runBlocking {
repository.getWeatherFromApi("", "")
repository.getWeatherFromApi(lat, long)
}
}
assertEquals(ioExceptionReturned.message, "Error Code: 400")
assertEquals(ioExceptionReturned.code(), 400)
assertEquals(ioExceptionReturned.message(), "Why dont you have a valid api key?")
}
@Test

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.helper
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
@@ -17,6 +17,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.IOException
import kotlin.properties.Delegates
class ServicesHelperTest : BaseTest() {
@@ -31,40 +32,45 @@ class ServicesHelperTest : BaseTest() {
@MockK
lateinit var locationProvider: LocationProviderImpl
lateinit var weatherResponse: WeatherResponse
lateinit var weatherResponse: WeatherApiResponse
private var lat by Delegates.notNull<Double>()
private var long by Delegates.notNull<Double>()
private lateinit var latlon: Pair<Double, Double>
private lateinit var fullWeather: FullWeather
@Before
fun setUp() {
MockKAnnotations.init(this)
helper = ServicesHelper(repository, settingsRepository, locationProvider)
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
weatherResponse = getTestData("new_response.json", WeatherApiResponse::class.java)
lat = weatherResponse.latitude!!
long = weatherResponse.longitude!!
latlon = Pair(lat, long)
fullWeather = weatherResponse.mapData().apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
}
@Test
fun testWidgetDataAsync_successfulResponse() = runBlocking {
// Arrange
val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
})
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Act
coEvery { locationProvider.getCurrentLatLong() } returns Pair(
weatherResponse.lat,
weatherResponse.lon
)
coEvery { locationProvider.getCurrentLatLong() } returns Pair(lat, long)
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery {
repository.getWeatherFromApi(
weatherResponse.lat.toString(),
weatherResponse.lon.toString()
lat.toString(),
long.toString()
)
}.returns(weatherResponse)
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon
lat,
long
)
}.returns(CURRENT_LOCATION)
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit

View File

@@ -4,7 +4,9 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import io.mockk.every
import io.mockk.mockk
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Response
@@ -29,9 +31,17 @@ open class BaseTest {
fun <T: Any> createErrorRetrofitMock(code: Int = 400): Response<T> {
val responseBody = mockk<ResponseBody>(relaxed = true)
val rawResponse = mockk<okhttp3.Response>().also {
every { it.code } returns code
}
return Response.error<T>(code, responseBody)
}
fun <T: Any> createErrorRetrofitMock(errorMessage: String, code: Int = 400): Response<T> {
val responseBody = errorMessage.toResponseBody("application/json".toMediaType())
val rawResponse = mockk<okhttp3.Response>(relaxed = true).also {
every { it.code } returns code
every { it.isSuccessful } returns false
every { it.body } returns responseBody
every { it.message } returns errorMessage
}
return Response.error<T>(responseBody, rawResponse)
}
}

View File

@@ -3,7 +3,7 @@ package com.appttude.h_mal.atlas_weather.viewmodel
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.model.ViewState
import com.appttude.h_mal.atlas_weather.model.types.LocationType
@@ -21,10 +21,10 @@ import io.mockk.impl.annotations.RelaxedMockK
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.anyList
import org.mockito.ArgumentMatchers.anyString
import java.io.IOException
import kotlin.properties.Delegates
import kotlin.test.assertEquals
import kotlin.test.assertIs
@@ -43,12 +43,23 @@ class WorldViewModelTest : BaseTest() {
@MockK
lateinit var locationProvider: LocationProviderImpl
private lateinit var weatherResponse: WeatherResponse
private lateinit var weatherResponse: WeatherApiResponse
private var lat by Delegates.notNull<Double>()
private var long by Delegates.notNull<Double>()
private lateinit var latlon: Pair<Double, Double>
private lateinit var fullWeather: FullWeather
@Before
fun setUp() {
MockKAnnotations.init(this)
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
weatherResponse = getTestData("new_response.json", WeatherApiResponse::class.java)
lat = weatherResponse.latitude!!
long = weatherResponse.longitude!!
latlon = Pair(lat, long)
fullWeather = weatherResponse.mapData().apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
}
@Test
@@ -58,13 +69,13 @@ class WorldViewModelTest : BaseTest() {
// Act
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
weatherResponse.lat,
weatherResponse.lon
lat,
long
)
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon,
lat,
long,
LocationType.City
)
}.returns(CURRENT_LOCATION)
@@ -74,7 +85,7 @@ class WorldViewModelTest : BaseTest() {
CURRENT_LOCATION,
locationType = LocationType.City
)
} returns FullWeather(weatherResponse)
} returns fullWeather
// Assert
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
@@ -86,17 +97,17 @@ class WorldViewModelTest : BaseTest() {
@Test
fun fetchDataForSingleLocation_failedLocation_validReturn() {
// Arrange
val errorMessage = ArgumentMatchers.anyString()
val errorMessage = anyString()
// Act
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
weatherResponse.lat,
weatherResponse.lon
lat,
long
)
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon,
lat,
long,
LocationType.City
)
} throws IOException(errorMessage)
@@ -112,18 +123,17 @@ class WorldViewModelTest : BaseTest() {
@Test
fun fetchDataForSingleLocation_failedApi_validReturn() {
// Arrange
val latlon = Pair(weatherResponse.lat, weatherResponse.lon)
val errorMessage = ArgumentMatchers.anyString()
val errorMessage = anyString()
// Act
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
weatherResponse.lat,
weatherResponse.lon
lat,
long
)
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon,
lat,
long,
LocationType.City
)
}.returns(CURRENT_LOCATION)
@@ -146,15 +156,14 @@ class WorldViewModelTest : BaseTest() {
@Test
fun fetchDataForSingleLocationSearch_validLocation_validReturn() {
// Arrange
val latlon = Pair(weatherResponse.lat, weatherResponse.lon)
// Act
every { weatherSource.repository.getSavedLocations() } returns anyList()
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon,
lat,
long,
LocationType.City
)
}.returns(CURRENT_LOCATION)
@@ -164,7 +173,7 @@ class WorldViewModelTest : BaseTest() {
CURRENT_LOCATION,
locationType = LocationType.City
)
} returns FullWeather(weatherResponse).apply { locationString = CURRENT_LOCATION }
} returns fullWeather.apply { locationString = CURRENT_LOCATION }
// Assert
viewModel.fetchDataForSingleLocationSearch(CURRENT_LOCATION)
@@ -192,7 +201,6 @@ class WorldViewModelTest : BaseTest() {
@Test
fun fetchDataForSingleLocationSearch_retrievedLocationExists_validError() {
// Arrange
val latlon = Pair(weatherResponse.lat, weatherResponse.lon)
val retrievedLocation = anyString()
// Act
@@ -200,8 +208,7 @@ class WorldViewModelTest : BaseTest() {
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
coEvery {
locationProvider.getLocationNameFromLatLong(
weatherResponse.lat,
weatherResponse.lon,
lat, long,
LocationType.City
)
}.returns(CURRENT_LOCATION)
@@ -211,7 +218,7 @@ class WorldViewModelTest : BaseTest() {
CURRENT_LOCATION,
locationType = LocationType.City
)
} returns FullWeather(weatherResponse).apply { locationString = retrievedLocation }
} returns fullWeather.apply { locationString = retrievedLocation }
// Assert
viewModel.fetchDataForSingleLocationSearch(CURRENT_LOCATION)

File diff suppressed because it is too large Load Diff

View File

@@ -1,317 +0,0 @@
{
"lat": 51.5,
"lon": -0.12,
"timezone": "Europe/London",
"timezone_offset": 0,
"current": {
"dt": 1608394319,
"sunrise": 1608364972,
"sunset": 1608393158,
"temp": 9.42,
"feels_like": 4.9,
"pressure": 1007,
"humidity": 91,
"dew_point": 8.03,
"uvi": 0,
"clouds": 33,
"visibility": 10000,
"wind_speed": 5.8,
"wind_deg": 215,
"weather": [
{
"id": 802,
"main": "Clouds",
"description": "scattered clouds",
"icon": "03n"
}
]
},
"daily": [
{
"dt": 1608375600,
"sunrise": 1608364972,
"sunset": 1608393158,
"temp": {
"day": 11.78,
"min": 9.1,
"max": 12.31,
"night": 9.1,
"eve": 9.8,
"morn": 10.8
},
"feels_like": {
"day": 6.46,
"night": 5.08,
"eve": 5.47,
"morn": 4.63
},
"pressure": 1005,
"humidity": 69,
"dew_point": 6.42,
"wind_speed": 6.37,
"wind_deg": 217,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"clouds": 97,
"pop": 0.98,
"rain": 1.55,
"uvi": 0.53
},
{
"dt": 1608462000,
"sunrise": 1608451406,
"sunset": 1608479581,
"temp": {
"day": 9.9,
"min": 7.41,
"max": 10.52,
"night": 7.41,
"eve": 8.83,
"morn": 8.59
},
"feels_like": {
"day": 4.19,
"night": 3.99,
"eve": 4.55,
"morn": 4.79
},
"pressure": 1013,
"humidity": 64,
"dew_point": 3.48,
"wind_speed": 6.12,
"wind_deg": 226,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"clouds": 8,
"pop": 0.58,
"rain": 0.65,
"uvi": 0.45
},
{
"dt": 1608548400,
"sunrise": 1608537838,
"sunset": 1608566008,
"temp": {
"day": 11.06,
"min": 7.01,
"max": 13.57,
"night": 13.2,
"eve": 12.68,
"morn": 8.59
},
"feels_like": {
"day": 6.32,
"night": 9.99,
"eve": 9.75,
"morn": 4.64
},
"pressure": 1005,
"humidity": 91,
"dew_point": 9.69,
"wind_speed": 6.7,
"wind_deg": 185,
"weather": [
{
"id": 501,
"main": "Rain",
"description": "moderate rain",
"icon": "10d"
}
],
"clouds": 100,
"pop": 1,
"rain": 7.85,
"uvi": 0.21
},
{
"dt": 1608634800,
"sunrise": 1608624266,
"sunset": 1608652438,
"temp": {
"day": 12.97,
"min": 11.7,
"max": 13.21,
"night": 11.7,
"eve": 12.37,
"morn": 12.93
},
"feels_like": {
"day": 11.39,
"night": 10.49,
"eve": 10.96,
"morn": 9.65
},
"pressure": 1012,
"humidity": 83,
"dew_point": 10.31,
"wind_speed": 2.38,
"wind_deg": 214,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"clouds": 100,
"pop": 1,
"rain": 3.25,
"uvi": 0.34
},
{
"dt": 1608721200,
"sunrise": 1608710690,
"sunset": 1608738871,
"temp": {
"day": 12.28,
"min": 10.12,
"max": 12.62,
"night": 10.12,
"eve": 10.12,
"morn": 11.73
},
"feels_like": {
"day": 7.48,
"night": 6.73,
"eve": 6.6,
"morn": 8.15
},
"pressure": 1006,
"humidity": 64,
"dew_point": 5.76,
"wind_speed": 5.45,
"wind_deg": 224,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"clouds": 97,
"pop": 0.94,
"rain": 2.52,
"uvi": 0.52
},
{
"dt": 1608811200,
"sunrise": 1608797112,
"sunset": 1608825307,
"temp": {
"day": 7.3,
"min": 4.66,
"max": 8.32,
"night": 4.66,
"eve": 5.76,
"morn": 5.85
},
"feels_like": {
"day": 0.36,
"night": -1.25,
"eve": -0.31,
"morn": -2.46
},
"pressure": 1020,
"humidity": 60,
"dew_point": 0.15,
"wind_speed": 7.09,
"wind_deg": 5,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d"
}
],
"clouds": 85,
"pop": 0.52,
"rain": 0.6,
"uvi": 1
},
{
"dt": 1608897600,
"sunrise": 1608883530,
"sunset": 1608911747,
"temp": {
"day": 4.12,
"min": 2.2,
"max": 4.63,
"night": 2.97,
"eve": 3.44,
"morn": 2.28
},
"feels_like": {
"day": -0.33,
"night": -0.43,
"eve": -0.1,
"morn": -2.82
},
"pressure": 1033,
"humidity": 70,
"dew_point": -3.09,
"wind_speed": 3.35,
"wind_deg": 334,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "clear sky",
"icon": "01d"
}
],
"clouds": 0,
"pop": 0,
"uvi": 1
},
{
"dt": 1608984000,
"sunrise": 1608969944,
"sunset": 1608998190,
"temp": {
"day": 6.03,
"min": 2.76,
"max": 6.92,
"night": 6.92,
"eve": 6.45,
"morn": 3.59
},
"feels_like": {
"day": 0.49,
"night": -0.96,
"eve": -0.28,
"morn": -0.44
},
"pressure": 1024,
"humidity": 69,
"dew_point": 0.84,
"wind_speed": 5.25,
"wind_deg": 251,
"weather": [
{
"id": 804,
"main": "Clouds",
"description": "overcast clouds",
"icon": "04d"
}
],
"clouds": 100,
"pop": 0,
"uvi": 1
}
]
}

View File

@@ -40,7 +40,7 @@ RETROFIT_VERSION = 2.9.0
OKHTTP_VERSION = 4.9.0
MOKITO_INLINE_VERSION = 2.13.0
CORE_TEST_VERSION = 2.2.0
MOCKK_VERSION = 1.10.5
MOCKK_VERSION = 1.13.12
TEST_JUNIT_VERSION = 1.2.0
TEST_RUNNER_VERSION = 1.5.2
ESPRESSO_VERSION = 3.6.0