diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1182c84..9de1fe2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { val kotlinVersion = "1.9.23" kotlin("jvm") version kotlinVersion @@ -15,8 +17,13 @@ dependencies { implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.springframework.boot:spring-boot-starter-mustache") + implementation("com.google.code.gson:gson:2.10.1") + implementation("org.danilopianini:khttp:1.6.1") testImplementation(kotlin("test")) + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("io.mockk:mockk:1.4.1") + implementation(kotlin("stdlib-jdk8")) } tasks.test { @@ -26,3 +33,11 @@ tasks.test { kotlin { jvmToolchain(17) } +val compileKotlin: KotlinCompile by tasks +compileKotlin.kotlinOptions { + jvmTarget = "17" +} +val compileTestKotlin: KotlinCompile by tasks +compileTestKotlin.kotlinOptions { + jvmTarget = "17" +} \ No newline at end of file diff --git a/app/src/main/kotlin/HtmlController.kt b/app/src/main/kotlin/HtmlController.kt new file mode 100644 index 0000000..1aae12c --- /dev/null +++ b/app/src/main/kotlin/HtmlController.kt @@ -0,0 +1,35 @@ +package hu.vanio.kotlin.feladat.ms + +import com.google.gson.Gson +import khttp.get +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.ui.set + +@Controller +class HtmlController { + + @Autowired + lateinit var weatherAppService: WeatherAppService + + @GetMapping("/") + fun index(model: Model): String { + model["title"] = "Average Daily temperature list" + val latitude = 47.4984 + val longitude = 19.0404 + val apiUrl = "https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude&hourly=temperature_2m&timezone=auto" + try { + val meteoData = weatherAppService.fetchMeteoData(apiUrl) + model["averageTempList"] = weatherAppService.displayTemperatureDailyAverage(meteoData) + } catch (exception: Exception) { + println("Exception occured while fetching meteo data. Exception message: " + exception.message) + model["averageTempList"] = arrayListOf("Exception occured while fetching meteo data.") + } + return "index" + } + + + +} diff --git a/app/src/main/kotlin/MeteoResponseData.kt b/app/src/main/kotlin/MeteoResponseData.kt new file mode 100644 index 0000000..48c65ea --- /dev/null +++ b/app/src/main/kotlin/MeteoResponseData.kt @@ -0,0 +1,23 @@ +package hu.vanio.kotlin.feladat.ms + +data class MeteoResponseData( + val latitude: Double, + val longitude: Double, + val generationtime_ms: Double, + val utc_offset_seconds: Int, + val timezone: String, + val timezone_abbreviation: String, + val elevation: Double, + val hourly_units: HourlyUnits, + val hourly: Hourly +) + +data class HourlyUnits( + val time: String, + val temperature_2m: String +) + +data class Hourly( + val time: List, + val temperature_2m: List +) diff --git a/app/src/main/kotlin/WeatherApp.kt b/app/src/main/kotlin/WeatherApp.kt index e0c098a..4dbdc63 100644 --- a/app/src/main/kotlin/WeatherApp.kt +++ b/app/src/main/kotlin/WeatherApp.kt @@ -2,11 +2,18 @@ package hu.vanio.kotlin.feladat.ms import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import com.google.gson.Gson +import khttp.get +import org.springframework.http.HttpStatusCode @SpringBootApplication class WeatherApp fun main() { runApplication() + println("STARTUP") + + } + diff --git a/app/src/main/kotlin/WeatherAppService.kt b/app/src/main/kotlin/WeatherAppService.kt new file mode 100644 index 0000000..71d40fc --- /dev/null +++ b/app/src/main/kotlin/WeatherAppService.kt @@ -0,0 +1,57 @@ +package hu.vanio.kotlin.feladat.ms + +import com.google.gson.Gson +import khttp.get +import org.springframework.http.HttpStatusCode +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.stereotype.Service +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.HttpServerErrorException + +@Service +class WeatherAppService { + fun displayTemperatureDailyAverage(meteoData: MeteoResponseData?): List { + val dailyAverage = mutableListOf() + if (meteoData != null) { + var dailySum = 0.0 + var actualDay = meteoData.hourly.time[0].substring(0, 10) + var dailyIndex = 0 + meteoData.hourly.temperature_2m.forEachIndexed { index, temperature -> + val dayToCheck = meteoData.hourly.time[index].substring(0, 10) + if (dayToCheck != actualDay) { + if (dailySum != 0.0) { + val actualAverage = actualDay + " temperature: " + dailySum / dailyIndex + println(actualAverage) + dailyAverage.add(actualAverage) + } + dailySum = 0.0 + dailyIndex = 0 + actualDay = dayToCheck + } + dailyIndex++ + dailySum += temperature + } + + //last day too + if (dailySum != 0.0) { + val actualAverage = actualDay + " temperature: " + dailySum / dailyIndex + println(actualAverage) + dailyAverage.add(actualAverage) + } + return dailyAverage + } else { + println("Failed to calculate the daily average temperature.") + throw Exception("Failed to calculate the daily average temperature.") + } + } + + fun fetchMeteoData(apiUrl: String): MeteoResponseData? { + val response = get(apiUrl) + if (response.statusCode == 200) { + return Gson().fromJson(response.text, MeteoResponseData::class.java) + } else { + throw HttpServerErrorException(HttpStatusCode.valueOf(response.statusCode)) + } + + } +} \ No newline at end of file diff --git a/app/src/main/resources/templates/footer.mustache b/app/src/main/resources/templates/footer.mustache new file mode 100644 index 0000000..691287b --- /dev/null +++ b/app/src/main/resources/templates/footer.mustache @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/resources/templates/header.mustache b/app/src/main/resources/templates/header.mustache new file mode 100644 index 0000000..71e0b08 --- /dev/null +++ b/app/src/main/resources/templates/header.mustache @@ -0,0 +1,5 @@ + + + {{title}} + + \ No newline at end of file diff --git a/app/src/main/resources/templates/index.mustache b/app/src/main/resources/templates/index.mustache new file mode 100644 index 0000000..63a5d64 --- /dev/null +++ b/app/src/main/resources/templates/index.mustache @@ -0,0 +1,9 @@ +{{> header}} + +

{{title}}

+ +{{#averageTempList}} +

{{.}}

+{{/averageTempList}} + +{{> footer}} \ No newline at end of file diff --git a/app/src/test/kotlin/WeatherAppServiceTest.kt b/app/src/test/kotlin/WeatherAppServiceTest.kt new file mode 100644 index 0000000..3d4459c --- /dev/null +++ b/app/src/test/kotlin/WeatherAppServiceTest.kt @@ -0,0 +1,70 @@ +package hu.vanio.kotlin.feladat.ms + +import com.google.gson.Gson +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.net.UnknownHostException + +class WeatherAppServiceTest { + + var weatherAppService = WeatherAppService() + @Test + fun fetchDataTest() { + val latitude = 47.4984 + val longitude = 19.0404 + val apiUrl = "https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude&hourly=temperature_2m&timezone=auto" + + val meteoData = weatherAppService.fetchMeteoData(apiUrl) + + assertTrue(meteoData != null) + assertEquals(24*7, meteoData!!.hourly.time.size) + assertEquals(24*7, meteoData!!.hourly.temperature_2m.size) + } + + @Test + fun fetchDataTestBadUrl() { + val apiUrl = "https://badApi" + assertThrows(UnknownHostException::class.java) { + weatherAppService.fetchMeteoData(apiUrl) + } + } + + @Test + fun displayTemperatureDailyAverageTest() { + val formattedString = """{ + "latitude":47.5, + "longitude":19.0625, + "generationtime_ms":0.026941299438476562, + "utc_offset_seconds":3600, + "timezone":"Europe/Budapest", + "timezone_abbreviation":"CET", + "elevation":124.0, + "hourly_units":{ + "time":"iso8601", + "temperature_2m":"°C" + }, + "hourly":{ + "time":[ + "2024-03-19T00:00", + "2024-03-19T01:00", + "2024-03-19T02:00", + "2024-03-20T02:00", + "2024-03-20T03:00", + "2024-03-20T04:00" + ], + "temperature_2m":[ + 3.4, + 3.2, + 2.9, + 2.7, + 2.5, + 2.6 + ] + } + }""" + val meteoResponse = Gson().fromJson(formattedString, MeteoResponseData::class.java) + val result = weatherAppService.displayTemperatureDailyAverage(meteoResponse) + assertEquals("2024-03-19 temperature: 3.1666666666666665", result[0]) + assertEquals("2024-03-20 temperature: 2.6", result[1]) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/WeatherAppTest.kt b/app/src/test/kotlin/WeatherAppTest.kt index a81a55a..2920d9d 100644 --- a/app/src/test/kotlin/WeatherAppTest.kt +++ b/app/src/test/kotlin/WeatherAppTest.kt @@ -1,11 +1,79 @@ package hu.vanio.kotlin.feladat.ms +import org.assertj.core.api.Assertions.assertThat +import org.mockito.Mock +import org.mockito.Mockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.boot.test.web.client.getForEntity +import org.springframework.http.HttpStatus import kotlin.test.Test +import org.mockito.Mockito.`when` +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.HttpStatusCode +import org.springframework.test.web.servlet.MockMvc +import org.springframework.web.client.HttpServerErrorException +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + + +@WebMvcTest(controllers = [HtmlController::class]) +class WeatherAppTest{ + + @MockBean + lateinit var weatherAppService: WeatherAppService + + @MockBean + lateinit var meteoResponseData: MeteoResponseData + + @Autowired + lateinit var mockMvc: MockMvc + + object MockitoHelper { + fun anyObject(): T { + Mockito.any() + return uninitialized() + } + @Suppress("UNCHECKED_CAST") + fun uninitialized(): T = null as T + } -class WeatherAppTest { @Test fun `sikeres lekerdezes`() { - TODO() + mockMvc.perform(get("/")).andExpect(status().isOk) + .andExpect(content() + .string("\n" + + "\n" + + " Average Daily temperature list\n" + + "\n" + + "\n" + + "\n" + + "

Average Daily temperature list

\n" + + "\n" + + "\n" + + "\n" + + "")) } + @Test fun `sikertelen lekerdezes`() { + `when`(weatherAppService.fetchMeteoData(MockitoHelper.anyObject())).thenThrow(HttpServerErrorException( + HttpStatusCode.valueOf(500))) + mockMvc.perform(get("/")).andExpect(status().isOk) + .andExpect(content() + .string("\n" + + "\n" + + " Average Daily temperature list\n" + + "\n" + + "\n" + + "\n" + + "

Average Daily temperature list

\n" + + "\n" + + "

Exception occured while fetching meteo data.

\n" + + "\n" + + "\n" + + "")) + } } \ No newline at end of file