From 3896d440ad204ddee298e10bce978074bd44d426 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bal=C3=A1zs=20Wittner?= <wittner.balazs@microsec.hu>
Date: Wed, 20 Mar 2024 08:47:21 +0100
Subject: [PATCH 1/3] 1st go

---
 app/build.gradle.kts                          |  12 +
 app/src/main/kotlin/WeatherApp.kt             |   7 +-
 app/src/main/kotlin/WeatherAppConfig.kt       |   8 +
 .../main/kotlin/WeatherCommandLineRunner.kt   |  24 ++
 .../main/kotlin/openmeteo/WeatherForecast.kt  |  31 ++
 .../main/kotlin/openmeteo/WeatherForecasts.kt |  18 +
 .../main/kotlin/openmeteo/WeatherService.kt   |  45 ++
 app/src/main/kotlin/vaadin/MainView.kt        |  38 ++
 app/src/main/resources/application.properties |  12 +
 .../openmeteo/WeatherMeanTimeServiceTest.kt   | 394 ++++++++++++++++++
 10 files changed, 588 insertions(+), 1 deletion(-)
 create mode 100644 app/src/main/kotlin/WeatherAppConfig.kt
 create mode 100644 app/src/main/kotlin/WeatherCommandLineRunner.kt
 create mode 100644 app/src/main/kotlin/openmeteo/WeatherForecast.kt
 create mode 100644 app/src/main/kotlin/openmeteo/WeatherForecasts.kt
 create mode 100644 app/src/main/kotlin/openmeteo/WeatherService.kt
 create mode 100644 app/src/main/kotlin/vaadin/MainView.kt
 create mode 100644 app/src/main/resources/application.properties
 create mode 100644 app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1182c84..42d7eaf 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,24 +1,32 @@
+import java.net.URI
+
 plugins {
     val kotlinVersion = "1.9.23"
     kotlin("jvm") version kotlinVersion
     kotlin("plugin.spring") version kotlinVersion
     id("org.springframework.boot") version "3.2.3"
+    id("com.vaadin") version "24.3.7"
 }
 
 group = "hu.kotlin.feladat.ms"
 
 repositories {
     mavenCentral()
+    maven { url = URI("https://maven.vaadin.com/vaadin-addons") }
 }
 
 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("com.vaadin:vaadin-core:24.+")
+    implementation("com.vaadin:vaadin-spring-boot-starter:24.+")
     testImplementation(kotlin("test"))
     testImplementation("io.mockk:mockk:1.4.1")
 }
 
+defaultTasks("clean", "vaadinBuildFrontend", "build")
+
 tasks.test {
     useJUnitPlatform()
 }
@@ -26,3 +34,7 @@ tasks.test {
 kotlin {
     jvmToolchain(17)
 }
+
+vaadin {
+    optimizeBundle = false
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/WeatherApp.kt b/app/src/main/kotlin/WeatherApp.kt
index e0c098a..aa04459 100644
--- a/app/src/main/kotlin/WeatherApp.kt
+++ b/app/src/main/kotlin/WeatherApp.kt
@@ -1,10 +1,15 @@
 package hu.vanio.kotlin.feladat.ms
 
+import com.vaadin.flow.component.page.AppShellConfigurator
+import com.vaadin.flow.theme.Theme
 import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.context.properties.EnableConfigurationProperties
 import org.springframework.boot.runApplication
 
 @SpringBootApplication
-class WeatherApp
+@EnableConfigurationProperties
+@Theme
+class WeatherApp: AppShellConfigurator
 
 fun main() {
     runApplication<WeatherApp>()
diff --git a/app/src/main/kotlin/WeatherAppConfig.kt b/app/src/main/kotlin/WeatherAppConfig.kt
new file mode 100644
index 0000000..845e47d
--- /dev/null
+++ b/app/src/main/kotlin/WeatherAppConfig.kt
@@ -0,0 +1,8 @@
+package hu.vanio.kotlin.feladat.ms
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.context.annotation.Configuration
+
+@Configuration
+@ConfigurationProperties(prefix = "weather")
+class WeatherAppConfig(var appUrl: String = "")
diff --git a/app/src/main/kotlin/WeatherCommandLineRunner.kt b/app/src/main/kotlin/WeatherCommandLineRunner.kt
new file mode 100644
index 0000000..9306531
--- /dev/null
+++ b/app/src/main/kotlin/WeatherCommandLineRunner.kt
@@ -0,0 +1,24 @@
+package hu.vanio.kotlin.feladat.ms
+
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecasts
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService
+import org.springframework.boot.CommandLineRunner
+import org.springframework.core.annotation.Order
+import org.springframework.stereotype.Component
+
+
+const val separator = "----------------------------------------------"
+
+@Component
+@Order(1)
+class WeatherCommandLineRunner(val weatherService: WeatherService): CommandLineRunner {
+
+    override fun run(vararg args: String?) {
+        print("$separator\n${weatherService.getWeatherStatistics().print()}\n$separator\n")
+    }
+}
+
+fun WeatherForecasts.print() =
+        "Weather forecast daily averages:\n$separator\n${this.weatherDailyForecast
+                .map { wdf -> "${wdf.day} | ${wdf.average()}"}
+                .reduce { first, second -> "${first}\n${second}"}}"
\ No newline at end of file
diff --git a/app/src/main/kotlin/openmeteo/WeatherForecast.kt b/app/src/main/kotlin/openmeteo/WeatherForecast.kt
new file mode 100644
index 0000000..90584fd
--- /dev/null
+++ b/app/src/main/kotlin/openmeteo/WeatherForecast.kt
@@ -0,0 +1,31 @@
+package hu.vanio.kotlin.feladat.ms.openmeteo
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class WeatherForecast (
+        val latitude: Double,
+        val longitude: Double,
+        @JsonProperty("generationtime_ms")
+        val generationtimeMs: Double,
+        @JsonProperty("utc_offset_seconds")
+        val utcOffsetSeconds: Long,
+        val timezone: String,
+        @JsonProperty("timezone_abbreviation")
+        val timezoneAbbreviation: String,
+        val elevation: Double,
+        @JsonProperty("hourly_units")
+        val hourlyUnits: HourlyUnits,
+        val hourly: Hourly,
+)
+
+data class HourlyUnits(
+        val time: String,
+        @JsonProperty("temperature_2m")
+        val temperature2m: String,
+)
+
+data class Hourly(
+        val time: List<String>,
+        @JsonProperty("temperature_2m")
+        val temperature2m: List<Double>,
+)
\ No newline at end of file
diff --git a/app/src/main/kotlin/openmeteo/WeatherForecasts.kt b/app/src/main/kotlin/openmeteo/WeatherForecasts.kt
new file mode 100644
index 0000000..7e8526b
--- /dev/null
+++ b/app/src/main/kotlin/openmeteo/WeatherForecasts.kt
@@ -0,0 +1,18 @@
+package hu.vanio.kotlin.feladat.ms.openmeteo
+
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.time.LocalDate
+
+data class WeatherDailyForecast(val day: LocalDate, private val temperatures: List<Double>) {
+    fun average(): Double = temperatures.average().round(2)
+
+    override fun toString(): String {
+        return "$day - ${average()}"
+    }
+}
+
+fun Double.round(digits: Int) =
+    BigDecimal(this).setScale(digits, RoundingMode.HALF_EVEN).toDouble()
+
+data class WeatherForecasts(val weatherDailyForecast: List<WeatherDailyForecast>)
\ No newline at end of file
diff --git a/app/src/main/kotlin/openmeteo/WeatherService.kt b/app/src/main/kotlin/openmeteo/WeatherService.kt
new file mode 100644
index 0000000..3acb9b7
--- /dev/null
+++ b/app/src/main/kotlin/openmeteo/WeatherService.kt
@@ -0,0 +1,45 @@
+package hu.vanio.kotlin.feladat.ms.openmeteo
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import hu.vanio.kotlin.feladat.ms.WeatherAppConfig
+import org.springframework.stereotype.Service
+
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+
+@Service
+class WeatherService(var config: WeatherAppConfig, var objectMapper: ObjectMapper) {
+    fun getWeatherStatistics(): WeatherForecasts {
+        val request = HttpRequest.newBuilder()
+                .uri(URI(config.appUrl))
+                .GET()
+                .build()
+
+        val weatherForecast = objectMapper.readValue(HttpClient.newHttpClient()
+                .send(request, HttpResponse.BodyHandlers.ofString()).body(), WeatherForecast::class.java)
+        return statistics(weatherForecast)
+    }
+
+    companion object {
+        fun statistics(weatherForecast: WeatherForecast): WeatherForecasts {
+            val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
+            val weatherMap = mutableMapOf<LocalDate, MutableList<Double>>()
+            var i = 0
+
+            weatherForecast.hourly.time.forEach {
+                val temperature = weatherForecast.hourly.temperature2m[i++]
+                val key = LocalDate.parse(it, dateTimeFormatter)
+                weatherMap.compute(key) { _, value ->
+                    value?.plus(temperature)?.toMutableList() ?: mutableListOf(temperature)
+                }
+            }
+            return WeatherForecasts(weatherMap.map {
+                WeatherDailyForecast(it.key, it.value)
+            }.toList())
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/vaadin/MainView.kt b/app/src/main/kotlin/vaadin/MainView.kt
new file mode 100644
index 0000000..f5361e2
--- /dev/null
+++ b/app/src/main/kotlin/vaadin/MainView.kt
@@ -0,0 +1,38 @@
+package hu.vanio.kotlin.feladat.ms.vaadin
+
+import com.vaadin.flow.component.Key
+import com.vaadin.flow.component.button.Button
+import com.vaadin.flow.component.grid.Grid
+import com.vaadin.flow.component.notification.Notification
+import com.vaadin.flow.component.orderedlayout.FlexComponent
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout
+import com.vaadin.flow.router.PageTitle
+import com.vaadin.flow.router.Route
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherDailyForecast
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService
+
+@PageTitle("Weather forecast")
+@Route
+class MainView(private val weatherService: WeatherService) : HorizontalLayout() {
+    private val table = Grid<WeatherDailyForecast>()
+    private val refresh = Button("Refresh").also {
+        it.addClickListener { _ ->
+            refreshTableContent()
+            Notification.show("Table content refreshed") }
+        it.addClickShortcut(Key.ENTER)
+    }
+
+    private fun refreshTableContent() {
+        val weatherStatistics = weatherService.getWeatherStatistics()
+        table.setItems(weatherStatistics.weatherDailyForecast)
+    }
+
+    init {
+        table.addColumn(WeatherDailyForecast::day).setHeader("Day")
+        table.addColumn(WeatherDailyForecast::average).setHeader("Daily average temperature")
+
+        super.setMargin(true)
+        super.setVerticalComponentAlignment(FlexComponent.Alignment.END, table, refresh)
+        super.add(table, refresh)
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties
new file mode 100644
index 0000000..5517427
--- /dev/null
+++ b/app/src/main/resources/application.properties
@@ -0,0 +1,12 @@
+weather.app-url=https://api.open-meteo.com/v1/forecast?latitude=47.4984&longitude=19.0404&hourly=temperature_2m&timezone=auto
+
+server.port=${PORT:8080}
+
+spring.mustache.check-template-location = false
+
+# Launch the default browser when starting the application in development mode
+vaadin.launch-browser=true
+# To improve the performance during development.
+# For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters
+vaadin.allowed-packages = com.vaadin,org.vaadin,dev.hilla,com.example.application
+spring.jpa.defer-datasource-initialization = true
diff --git a/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt b/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt
new file mode 100644
index 0000000..d23ea17
--- /dev/null
+++ b/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt
@@ -0,0 +1,394 @@
+package openmeteo
+
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecast
+import org.junit.jupiter.api.Test
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+
+class WeatherMeanTimeServiceTest() {
+    private val objectMapper = ObjectMapper().also {
+        it.registerModules(JavaTimeModule(), ParameterNamesModule())
+        it.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false)
+    }
+
+    val response = """
+        {
+  "latitude": 47.5,
+  "longitude": 19.0625,
+  "generationtime_ms": 0.030040740966796875,
+  "utc_offset_seconds": 3600,
+  "timezone": "Europe/Budapest",
+  "timezone_abbreviation": "CET",
+  "elevation": 124,
+  "hourly_units": {
+    "time": "iso8601",
+    "temperature_2m": "°C"
+  },
+  "hourly": {
+    "time": [
+      "2024-03-18T00:00",
+      "2024-03-18T01:00",
+      "2024-03-18T02:00",
+      "2024-03-18T03:00",
+      "2024-03-18T04:00",
+      "2024-03-18T05:00",
+      "2024-03-18T06:00",
+      "2024-03-18T07:00",
+      "2024-03-18T08:00",
+      "2024-03-18T09:00",
+      "2024-03-18T10:00",
+      "2024-03-18T11:00",
+      "2024-03-18T12:00",
+      "2024-03-18T13:00",
+      "2024-03-18T14:00",
+      "2024-03-18T15:00",
+      "2024-03-18T16:00",
+      "2024-03-18T17:00",
+      "2024-03-18T18:00",
+      "2024-03-18T19:00",
+      "2024-03-18T20:00",
+      "2024-03-18T21:00",
+      "2024-03-18T22:00",
+      "2024-03-18T23:00",
+      "2024-03-19T00:00",
+      "2024-03-19T01:00",
+      "2024-03-19T02:00",
+      "2024-03-19T03:00",
+      "2024-03-19T04:00",
+      "2024-03-19T05:00",
+      "2024-03-19T06:00",
+      "2024-03-19T07:00",
+      "2024-03-19T08:00",
+      "2024-03-19T09:00",
+      "2024-03-19T10:00",
+      "2024-03-19T11:00",
+      "2024-03-19T12:00",
+      "2024-03-19T13:00",
+      "2024-03-19T14:00",
+      "2024-03-19T15:00",
+      "2024-03-19T16:00",
+      "2024-03-19T17:00",
+      "2024-03-19T18:00",
+      "2024-03-19T19:00",
+      "2024-03-19T20:00",
+      "2024-03-19T21:00",
+      "2024-03-19T22:00",
+      "2024-03-19T23:00",
+      "2024-03-20T00:00",
+      "2024-03-20T01:00",
+      "2024-03-20T02:00",
+      "2024-03-20T03:00",
+      "2024-03-20T04:00",
+      "2024-03-20T05:00",
+      "2024-03-20T06:00",
+      "2024-03-20T07:00",
+      "2024-03-20T08:00",
+      "2024-03-20T09:00",
+      "2024-03-20T10:00",
+      "2024-03-20T11:00",
+      "2024-03-20T12:00",
+      "2024-03-20T13:00",
+      "2024-03-20T14:00",
+      "2024-03-20T15:00",
+      "2024-03-20T16:00",
+      "2024-03-20T17:00",
+      "2024-03-20T18:00",
+      "2024-03-20T19:00",
+      "2024-03-20T20:00",
+      "2024-03-20T21:00",
+      "2024-03-20T22:00",
+      "2024-03-20T23:00",
+      "2024-03-21T00:00",
+      "2024-03-21T01:00",
+      "2024-03-21T02:00",
+      "2024-03-21T03:00",
+      "2024-03-21T04:00",
+      "2024-03-21T05:00",
+      "2024-03-21T06:00",
+      "2024-03-21T07:00",
+      "2024-03-21T08:00",
+      "2024-03-21T09:00",
+      "2024-03-21T10:00",
+      "2024-03-21T11:00",
+      "2024-03-21T12:00",
+      "2024-03-21T13:00",
+      "2024-03-21T14:00",
+      "2024-03-21T15:00",
+      "2024-03-21T16:00",
+      "2024-03-21T17:00",
+      "2024-03-21T18:00",
+      "2024-03-21T19:00",
+      "2024-03-21T20:00",
+      "2024-03-21T21:00",
+      "2024-03-21T22:00",
+      "2024-03-21T23:00",
+      "2024-03-22T00:00",
+      "2024-03-22T01:00",
+      "2024-03-22T02:00",
+      "2024-03-22T03:00",
+      "2024-03-22T04:00",
+      "2024-03-22T05:00",
+      "2024-03-22T06:00",
+      "2024-03-22T07:00",
+      "2024-03-22T08:00",
+      "2024-03-22T09:00",
+      "2024-03-22T10:00",
+      "2024-03-22T11:00",
+      "2024-03-22T12:00",
+      "2024-03-22T13:00",
+      "2024-03-22T14:00",
+      "2024-03-22T15:00",
+      "2024-03-22T16:00",
+      "2024-03-22T17:00",
+      "2024-03-22T18:00",
+      "2024-03-22T19:00",
+      "2024-03-22T20:00",
+      "2024-03-22T21:00",
+      "2024-03-22T22:00",
+      "2024-03-22T23:00",
+      "2024-03-23T00:00",
+      "2024-03-23T01:00",
+      "2024-03-23T02:00",
+      "2024-03-23T03:00",
+      "2024-03-23T04:00",
+      "2024-03-23T05:00",
+      "2024-03-23T06:00",
+      "2024-03-23T07:00",
+      "2024-03-23T08:00",
+      "2024-03-23T09:00",
+      "2024-03-23T10:00",
+      "2024-03-23T11:00",
+      "2024-03-23T12:00",
+      "2024-03-23T13:00",
+      "2024-03-23T14:00",
+      "2024-03-23T15:00",
+      "2024-03-23T16:00",
+      "2024-03-23T17:00",
+      "2024-03-23T18:00",
+      "2024-03-23T19:00",
+      "2024-03-23T20:00",
+      "2024-03-23T21:00",
+      "2024-03-23T22:00",
+      "2024-03-23T23:00",
+      "2024-03-24T00:00",
+      "2024-03-24T01:00",
+      "2024-03-24T02:00",
+      "2024-03-24T03:00",
+      "2024-03-24T04:00",
+      "2024-03-24T05:00",
+      "2024-03-24T06:00",
+      "2024-03-24T07:00",
+      "2024-03-24T08:00",
+      "2024-03-24T09:00",
+      "2024-03-24T10:00",
+      "2024-03-24T11:00",
+      "2024-03-24T12:00",
+      "2024-03-24T13:00",
+      "2024-03-24T14:00",
+      "2024-03-24T15:00",
+      "2024-03-24T16:00",
+      "2024-03-24T17:00",
+      "2024-03-24T18:00",
+      "2024-03-24T19:00",
+      "2024-03-24T20:00",
+      "2024-03-24T21:00",
+      "2024-03-24T22:00",
+      "2024-03-24T23:00"
+    ],
+    "temperature_2m": [
+      6,
+      5.7,
+      5.4,
+      5.2,
+      4.8,
+      4.6,
+      4.5,
+      4.7,
+      5.9,
+      7.3,
+      8.9,
+      9.7,
+      10.4,
+      10.9,
+      11.1,
+      11,
+      10.6,
+      9.8,
+      8.7,
+      7.4,
+      6.2,
+      5.4,
+      4.7,
+      4.2,
+      3.7,
+      3.3,
+      3,
+      2.9,
+      2.8,
+      2.8,
+      2.8,
+      3.2,
+      3.9,
+      4.7,
+      5.5,
+      6.4,
+      7.4,
+      8,
+      8.5,
+      8.8,
+      8.8,
+      8.6,
+      7.8,
+      6.7,
+      5.6,
+      4.7,
+      4,
+      3.4,
+      2.8,
+      2.3,
+      1.9,
+      1.6,
+      1.3,
+      1,
+      0.8,
+      1.1,
+      3.5,
+      6.8,
+      9.1,
+      10.5,
+      11.5,
+      12.2,
+      12.8,
+      13.1,
+      12.9,
+      12.3,
+      10.9,
+      9.1,
+      7.8,
+      6.9,
+      6,
+      5.4,
+      4.6,
+      4.3,
+      4.1,
+      4.1,
+      4.2,
+      4.3,
+      4.1,
+      4.1,
+      6.1,
+      8.3,
+      10.1,
+      11.5,
+      12.6,
+      13.3,
+      13.9,
+      14.2,
+      14.1,
+      13.4,
+      12.4,
+      11.4,
+      10.7,
+      10.1,
+      9.5,
+      9.1,
+      8.8,
+      8.5,
+      8.2,
+      8,
+      7.8,
+      7.4,
+      7,
+      7.2,
+      8.2,
+      9.6,
+      11.2,
+      12.7,
+      14.3,
+      15.5,
+      16.2,
+      16.4,
+      16.1,
+      15.3,
+      14,
+      12.7,
+      11.7,
+      10.7,
+      9.9,
+      9.4,
+      9.1,
+      8.9,
+      8.7,
+      8.5,
+      8.3,
+      8.1,
+      7.9,
+      7.7,
+      10.8,
+      12.7,
+      14.5,
+      16.4,
+      18.3,
+      19.7,
+      20.4,
+      20.6,
+      20.3,
+      19.5,
+      18.2,
+      16.7,
+      14.8,
+      12.7,
+      10.9,
+      9.9,
+      9.3,
+      8.7,
+      8.2,
+      7.8,
+      7.5,
+      7.2,
+      7,
+      6.9,
+      7.1,
+      7.5,
+      7.8,
+      7.8,
+      7.6,
+      7.5,
+      7.4,
+      7.4,
+      7.4,
+      7.2,
+      6.9,
+      6.8,
+      6.8,
+      7,
+      7.1,
+      7.1
+    ]
+  }
+}
+    """.trimIndent()
+
+    @Test
+    fun test() {
+        val weatherForecast = objectMapper.readValue(response, WeatherForecast::class.java)
+        val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
+        val weatherMap = mutableMapOf<LocalDate, MutableList<Double>>()
+
+        var i = 0
+        weatherForecast.hourly.time.forEach {
+            val temperature = weatherForecast.hourly.temperature2m[i++]
+            val key = LocalDate.parse(it, dateTimeFormatter)
+            weatherMap.compute(key) { _, value ->
+                value?.plus(temperature)?.toMutableList() ?: mutableListOf(temperature)
+            }
+        }
+        weatherMap.forEach {
+            println("${it.key} - ${it.value.average()}")
+        }
+    }
+}
\ No newline at end of file

From 8023b88551917a19a0eebb22609cadb905d248e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bal=C3=A1zs=20Wittner?= <wittner.balazs@microsec.hu>
Date: Wed, 20 Mar 2024 10:10:38 +0100
Subject: [PATCH 2/3] Error handling

---
 .../main/kotlin/WeatherCommandLineRunner.kt    |  9 +++++++--
 .../kotlin/exception/ServiceUnavailable.kt     |  6 ++++++
 .../main/kotlin/openmeteo/WeatherService.kt    | 10 ++++++++--
 .../main/kotlin/vaadin/CustomErrorHandler.kt   | 18 ++++++++++++++++++
 .../main/kotlin/vaadin/VaadinInitListener.kt   | 13 +++++++++++++
 5 files changed, 52 insertions(+), 4 deletions(-)
 create mode 100644 app/src/main/kotlin/exception/ServiceUnavailable.kt
 create mode 100644 app/src/main/kotlin/vaadin/CustomErrorHandler.kt
 create mode 100644 app/src/main/kotlin/vaadin/VaadinInitListener.kt

diff --git a/app/src/main/kotlin/WeatherCommandLineRunner.kt b/app/src/main/kotlin/WeatherCommandLineRunner.kt
index 9306531..40361ab 100644
--- a/app/src/main/kotlin/WeatherCommandLineRunner.kt
+++ b/app/src/main/kotlin/WeatherCommandLineRunner.kt
@@ -1,5 +1,6 @@
 package hu.vanio.kotlin.feladat.ms
 
+import hu.vanio.kotlin.feladat.ms.exception.ServiceUnavailable
 import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecasts
 import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService
 import org.springframework.boot.CommandLineRunner
@@ -14,11 +15,15 @@ const val separator = "----------------------------------------------"
 class WeatherCommandLineRunner(val weatherService: WeatherService): CommandLineRunner {
 
     override fun run(vararg args: String?) {
-        print("$separator\n${weatherService.getWeatherStatistics().print()}\n$separator\n")
+        val weatherStatistics = try { weatherService.getWeatherStatistics() } catch (e: ServiceUnavailable) {
+            println("Weather forecast service is unavailable")
+            return
+        }
+        println("$separator\n${weatherStatistics.println()}$separator")
     }
 }
 
-fun WeatherForecasts.print() =
+fun WeatherForecasts.println() =
         "Weather forecast daily averages:\n$separator\n${this.weatherDailyForecast
                 .map { wdf -> "${wdf.day} | ${wdf.average()}"}
                 .reduce { first, second -> "${first}\n${second}"}}"
\ No newline at end of file
diff --git a/app/src/main/kotlin/exception/ServiceUnavailable.kt b/app/src/main/kotlin/exception/ServiceUnavailable.kt
new file mode 100644
index 0000000..ba5ec5b
--- /dev/null
+++ b/app/src/main/kotlin/exception/ServiceUnavailable.kt
@@ -0,0 +1,6 @@
+package hu.vanio.kotlin.feladat.ms.exception
+
+class ServiceUnavailable(private val serviceId: String): RuntimeException() {
+    override val message: String
+        get() = "Service $serviceId is unavailable"
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/openmeteo/WeatherService.kt b/app/src/main/kotlin/openmeteo/WeatherService.kt
index 3acb9b7..569dae1 100644
--- a/app/src/main/kotlin/openmeteo/WeatherService.kt
+++ b/app/src/main/kotlin/openmeteo/WeatherService.kt
@@ -2,7 +2,9 @@ package hu.vanio.kotlin.feladat.ms.openmeteo
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import hu.vanio.kotlin.feladat.ms.WeatherAppConfig
+import hu.vanio.kotlin.feladat.ms.exception.ServiceUnavailable
 import org.springframework.stereotype.Service
+import java.io.IOException
 
 import java.net.URI
 import java.net.http.HttpClient
@@ -13,14 +15,18 @@ import java.time.format.DateTimeFormatter
 
 @Service
 class WeatherService(var config: WeatherAppConfig, var objectMapper: ObjectMapper) {
+    private val id = "WEATHER_SERVICE"
+
     fun getWeatherStatistics(): WeatherForecasts {
         val request = HttpRequest.newBuilder()
                 .uri(URI(config.appUrl))
                 .GET()
                 .build()
-
-        val weatherForecast = objectMapper.readValue(HttpClient.newHttpClient()
+        val weatherForecast = try {objectMapper.readValue(HttpClient.newHttpClient()
                 .send(request, HttpResponse.BodyHandlers.ofString()).body(), WeatherForecast::class.java)
+        } catch (e: IOException) {
+            throw ServiceUnavailable(id)
+        }
         return statistics(weatherForecast)
     }
 
diff --git a/app/src/main/kotlin/vaadin/CustomErrorHandler.kt b/app/src/main/kotlin/vaadin/CustomErrorHandler.kt
new file mode 100644
index 0000000..3b3f3d8
--- /dev/null
+++ b/app/src/main/kotlin/vaadin/CustomErrorHandler.kt
@@ -0,0 +1,18 @@
+package hu.vanio.kotlin.feladat.ms.vaadin
+
+import com.vaadin.flow.component.UI
+import com.vaadin.flow.component.notification.Notification
+import com.vaadin.flow.server.ErrorEvent
+import com.vaadin.flow.server.ErrorHandler
+import org.springframework.stereotype.Component
+
+@Component
+class CustomErrorHandler : ErrorHandler {
+    override fun error(errorEvent: ErrorEvent) {
+        if (UI.getCurrent() != null) {
+            UI.getCurrent().access {
+                Notification.show("An internal error has occurred. (${errorEvent.throwable.message})")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/vaadin/VaadinInitListener.kt b/app/src/main/kotlin/vaadin/VaadinInitListener.kt
new file mode 100644
index 0000000..d51f747
--- /dev/null
+++ b/app/src/main/kotlin/vaadin/VaadinInitListener.kt
@@ -0,0 +1,13 @@
+package hu.vanio.kotlin.feladat.ms.vaadin
+
+import com.vaadin.flow.server.ServiceInitEvent
+import com.vaadin.flow.server.SessionInitEvent
+import com.vaadin.flow.server.VaadinServiceInitListener
+import org.springframework.stereotype.Component
+
+@Component
+class VaadinInitListener(val customErrorHandler: CustomErrorHandler) : VaadinServiceInitListener {
+    override fun serviceInit(serviceEvent: ServiceInitEvent) {
+        serviceEvent.source.addSessionInitListener { initEvent: SessionInitEvent? -> initEvent?.session?.errorHandler = customErrorHandler}
+    }
+}
\ No newline at end of file

From 36e53c5c71ab2260825ce723f0fbba7a62f68613 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bal=C3=A1zs=20Wittner?= <wittner.balazs@microsec.hu>
Date: Wed, 20 Mar 2024 12:49:01 +0100
Subject: [PATCH 3/3] Tests

---
 app/build.gradle.kts                          |   3 +-
 .../main/kotlin/WeatherCommandLineRunner.kt   |   2 +-
 app/src/test/kotlin/WeatherAppTest.kt         |  11 -
 .../kotlin/WeatherCommandLineRunnerTest.kt    |  61 +++
 .../openmeteo/WeatherMeanTimeServiceTest.kt   | 394 ------------------
 .../kotlin/openmeteo/WeatherServiceTest.kt    | 133 ++++++
 6 files changed, 197 insertions(+), 407 deletions(-)
 delete mode 100644 app/src/test/kotlin/WeatherAppTest.kt
 create mode 100644 app/src/test/kotlin/WeatherCommandLineRunnerTest.kt
 delete mode 100644 app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt
 create mode 100644 app/src/test/kotlin/openmeteo/WeatherServiceTest.kt

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 42d7eaf..8f50f01 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,7 +22,8 @@ dependencies {
     implementation("com.vaadin:vaadin-core:24.+")
     implementation("com.vaadin:vaadin-spring-boot-starter:24.+")
     testImplementation(kotlin("test"))
-    testImplementation("io.mockk:mockk:1.4.1")
+    testImplementation("io.mockk:mockk:1.9.3")
+    testImplementation("org.springframework.boot:spring-boot-starter-test")
 }
 
 defaultTasks("clean", "vaadinBuildFrontend", "build")
diff --git a/app/src/main/kotlin/WeatherCommandLineRunner.kt b/app/src/main/kotlin/WeatherCommandLineRunner.kt
index 40361ab..c5dffa4 100644
--- a/app/src/main/kotlin/WeatherCommandLineRunner.kt
+++ b/app/src/main/kotlin/WeatherCommandLineRunner.kt
@@ -26,4 +26,4 @@ class WeatherCommandLineRunner(val weatherService: WeatherService): CommandLineR
 fun WeatherForecasts.println() =
         "Weather forecast daily averages:\n$separator\n${this.weatherDailyForecast
                 .map { wdf -> "${wdf.day} | ${wdf.average()}"}
-                .reduce { first, second -> "${first}\n${second}"}}"
\ No newline at end of file
+                .reduce { first, second -> "${first}\n${second}"}}\n"
\ No newline at end of file
diff --git a/app/src/test/kotlin/WeatherAppTest.kt b/app/src/test/kotlin/WeatherAppTest.kt
deleted file mode 100644
index a81a55a..0000000
--- a/app/src/test/kotlin/WeatherAppTest.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package hu.vanio.kotlin.feladat.ms
-
-import kotlin.test.Test
-
-class WeatherAppTest {
-
-    @Test fun `sikeres lekerdezes`() {
-        TODO()
-    }
-
-}
\ No newline at end of file
diff --git a/app/src/test/kotlin/WeatherCommandLineRunnerTest.kt b/app/src/test/kotlin/WeatherCommandLineRunnerTest.kt
new file mode 100644
index 0000000..4e5a6d9
--- /dev/null
+++ b/app/src/test/kotlin/WeatherCommandLineRunnerTest.kt
@@ -0,0 +1,61 @@
+
+import hu.vanio.kotlin.feladat.ms.WeatherCommandLineRunner
+import hu.vanio.kotlin.feladat.ms.exception.ServiceUnavailable
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherDailyForecast
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecasts
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import java.time.LocalDate
+import java.time.Month
+
+class WeatherCommandLineRunnerTest {
+    @MockK
+    lateinit var weatherService: WeatherService
+
+    @BeforeEach
+    fun setUp() = MockKAnnotations.init(this)
+
+    @Test
+    fun testConsoleLog() {
+        every { weatherService.getWeatherStatistics() } returns WeatherForecasts(listOf(
+                WeatherDailyForecast(LocalDate.of(2024, Month.MARCH, 1), listOf(1.0, 2.0, 3.0))))
+        val weatherCommandLineRunner = WeatherCommandLineRunner(weatherService)
+
+        assertStandardOutput("----------------------------------------------\n" +
+                "Weather forecast daily averages:\n" +
+                "----------------------------------------------\n" +
+                "2024-03-01 | 2.0\n" +
+                "----------------------------------------------") { weatherCommandLineRunner.run() }
+    }
+
+    @Test
+    fun testServiceUnavailable() {
+        every { weatherService.getWeatherStatistics() } throws ServiceUnavailable("")
+        val weatherCommandLineRunner = WeatherCommandLineRunner(weatherService)
+
+        assertStandardOutput("Weather forecast service is unavailable") { weatherCommandLineRunner.run() }
+    }
+
+    private fun assertStandardOutput(expectedStdOut: String, functionCall: Runnable) {
+        val outputStream = ByteArrayOutputStream()
+        val printStream = PrintStream(outputStream)
+        val originalOut = System.out
+
+        try {
+            System.setOut(printStream)
+
+            functionCall.run()
+
+            assertEquals(expectedStdOut, outputStream.toString().trim())
+        } finally {
+            System.setOut(originalOut)
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt b/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt
deleted file mode 100644
index d23ea17..0000000
--- a/app/src/test/kotlin/openmeteo/WeatherMeanTimeServiceTest.kt
+++ /dev/null
@@ -1,394 +0,0 @@
-package openmeteo
-
-import com.fasterxml.jackson.databind.DeserializationFeature
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
-import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecast
-import org.junit.jupiter.api.Test
-import java.time.LocalDate
-import java.time.format.DateTimeFormatter
-
-class WeatherMeanTimeServiceTest() {
-    private val objectMapper = ObjectMapper().also {
-        it.registerModules(JavaTimeModule(), ParameterNamesModule())
-        it.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, false)
-    }
-
-    val response = """
-        {
-  "latitude": 47.5,
-  "longitude": 19.0625,
-  "generationtime_ms": 0.030040740966796875,
-  "utc_offset_seconds": 3600,
-  "timezone": "Europe/Budapest",
-  "timezone_abbreviation": "CET",
-  "elevation": 124,
-  "hourly_units": {
-    "time": "iso8601",
-    "temperature_2m": "°C"
-  },
-  "hourly": {
-    "time": [
-      "2024-03-18T00:00",
-      "2024-03-18T01:00",
-      "2024-03-18T02:00",
-      "2024-03-18T03:00",
-      "2024-03-18T04:00",
-      "2024-03-18T05:00",
-      "2024-03-18T06:00",
-      "2024-03-18T07:00",
-      "2024-03-18T08:00",
-      "2024-03-18T09:00",
-      "2024-03-18T10:00",
-      "2024-03-18T11:00",
-      "2024-03-18T12:00",
-      "2024-03-18T13:00",
-      "2024-03-18T14:00",
-      "2024-03-18T15:00",
-      "2024-03-18T16:00",
-      "2024-03-18T17:00",
-      "2024-03-18T18:00",
-      "2024-03-18T19:00",
-      "2024-03-18T20:00",
-      "2024-03-18T21:00",
-      "2024-03-18T22:00",
-      "2024-03-18T23:00",
-      "2024-03-19T00:00",
-      "2024-03-19T01:00",
-      "2024-03-19T02:00",
-      "2024-03-19T03:00",
-      "2024-03-19T04:00",
-      "2024-03-19T05:00",
-      "2024-03-19T06:00",
-      "2024-03-19T07:00",
-      "2024-03-19T08:00",
-      "2024-03-19T09:00",
-      "2024-03-19T10:00",
-      "2024-03-19T11:00",
-      "2024-03-19T12:00",
-      "2024-03-19T13:00",
-      "2024-03-19T14:00",
-      "2024-03-19T15:00",
-      "2024-03-19T16:00",
-      "2024-03-19T17:00",
-      "2024-03-19T18:00",
-      "2024-03-19T19:00",
-      "2024-03-19T20:00",
-      "2024-03-19T21:00",
-      "2024-03-19T22:00",
-      "2024-03-19T23:00",
-      "2024-03-20T00:00",
-      "2024-03-20T01:00",
-      "2024-03-20T02:00",
-      "2024-03-20T03:00",
-      "2024-03-20T04:00",
-      "2024-03-20T05:00",
-      "2024-03-20T06:00",
-      "2024-03-20T07:00",
-      "2024-03-20T08:00",
-      "2024-03-20T09:00",
-      "2024-03-20T10:00",
-      "2024-03-20T11:00",
-      "2024-03-20T12:00",
-      "2024-03-20T13:00",
-      "2024-03-20T14:00",
-      "2024-03-20T15:00",
-      "2024-03-20T16:00",
-      "2024-03-20T17:00",
-      "2024-03-20T18:00",
-      "2024-03-20T19:00",
-      "2024-03-20T20:00",
-      "2024-03-20T21:00",
-      "2024-03-20T22:00",
-      "2024-03-20T23:00",
-      "2024-03-21T00:00",
-      "2024-03-21T01:00",
-      "2024-03-21T02:00",
-      "2024-03-21T03:00",
-      "2024-03-21T04:00",
-      "2024-03-21T05:00",
-      "2024-03-21T06:00",
-      "2024-03-21T07:00",
-      "2024-03-21T08:00",
-      "2024-03-21T09:00",
-      "2024-03-21T10:00",
-      "2024-03-21T11:00",
-      "2024-03-21T12:00",
-      "2024-03-21T13:00",
-      "2024-03-21T14:00",
-      "2024-03-21T15:00",
-      "2024-03-21T16:00",
-      "2024-03-21T17:00",
-      "2024-03-21T18:00",
-      "2024-03-21T19:00",
-      "2024-03-21T20:00",
-      "2024-03-21T21:00",
-      "2024-03-21T22:00",
-      "2024-03-21T23:00",
-      "2024-03-22T00:00",
-      "2024-03-22T01:00",
-      "2024-03-22T02:00",
-      "2024-03-22T03:00",
-      "2024-03-22T04:00",
-      "2024-03-22T05:00",
-      "2024-03-22T06:00",
-      "2024-03-22T07:00",
-      "2024-03-22T08:00",
-      "2024-03-22T09:00",
-      "2024-03-22T10:00",
-      "2024-03-22T11:00",
-      "2024-03-22T12:00",
-      "2024-03-22T13:00",
-      "2024-03-22T14:00",
-      "2024-03-22T15:00",
-      "2024-03-22T16:00",
-      "2024-03-22T17:00",
-      "2024-03-22T18:00",
-      "2024-03-22T19:00",
-      "2024-03-22T20:00",
-      "2024-03-22T21:00",
-      "2024-03-22T22:00",
-      "2024-03-22T23:00",
-      "2024-03-23T00:00",
-      "2024-03-23T01:00",
-      "2024-03-23T02:00",
-      "2024-03-23T03:00",
-      "2024-03-23T04:00",
-      "2024-03-23T05:00",
-      "2024-03-23T06:00",
-      "2024-03-23T07:00",
-      "2024-03-23T08:00",
-      "2024-03-23T09:00",
-      "2024-03-23T10:00",
-      "2024-03-23T11:00",
-      "2024-03-23T12:00",
-      "2024-03-23T13:00",
-      "2024-03-23T14:00",
-      "2024-03-23T15:00",
-      "2024-03-23T16:00",
-      "2024-03-23T17:00",
-      "2024-03-23T18:00",
-      "2024-03-23T19:00",
-      "2024-03-23T20:00",
-      "2024-03-23T21:00",
-      "2024-03-23T22:00",
-      "2024-03-23T23:00",
-      "2024-03-24T00:00",
-      "2024-03-24T01:00",
-      "2024-03-24T02:00",
-      "2024-03-24T03:00",
-      "2024-03-24T04:00",
-      "2024-03-24T05:00",
-      "2024-03-24T06:00",
-      "2024-03-24T07:00",
-      "2024-03-24T08:00",
-      "2024-03-24T09:00",
-      "2024-03-24T10:00",
-      "2024-03-24T11:00",
-      "2024-03-24T12:00",
-      "2024-03-24T13:00",
-      "2024-03-24T14:00",
-      "2024-03-24T15:00",
-      "2024-03-24T16:00",
-      "2024-03-24T17:00",
-      "2024-03-24T18:00",
-      "2024-03-24T19:00",
-      "2024-03-24T20:00",
-      "2024-03-24T21:00",
-      "2024-03-24T22:00",
-      "2024-03-24T23:00"
-    ],
-    "temperature_2m": [
-      6,
-      5.7,
-      5.4,
-      5.2,
-      4.8,
-      4.6,
-      4.5,
-      4.7,
-      5.9,
-      7.3,
-      8.9,
-      9.7,
-      10.4,
-      10.9,
-      11.1,
-      11,
-      10.6,
-      9.8,
-      8.7,
-      7.4,
-      6.2,
-      5.4,
-      4.7,
-      4.2,
-      3.7,
-      3.3,
-      3,
-      2.9,
-      2.8,
-      2.8,
-      2.8,
-      3.2,
-      3.9,
-      4.7,
-      5.5,
-      6.4,
-      7.4,
-      8,
-      8.5,
-      8.8,
-      8.8,
-      8.6,
-      7.8,
-      6.7,
-      5.6,
-      4.7,
-      4,
-      3.4,
-      2.8,
-      2.3,
-      1.9,
-      1.6,
-      1.3,
-      1,
-      0.8,
-      1.1,
-      3.5,
-      6.8,
-      9.1,
-      10.5,
-      11.5,
-      12.2,
-      12.8,
-      13.1,
-      12.9,
-      12.3,
-      10.9,
-      9.1,
-      7.8,
-      6.9,
-      6,
-      5.4,
-      4.6,
-      4.3,
-      4.1,
-      4.1,
-      4.2,
-      4.3,
-      4.1,
-      4.1,
-      6.1,
-      8.3,
-      10.1,
-      11.5,
-      12.6,
-      13.3,
-      13.9,
-      14.2,
-      14.1,
-      13.4,
-      12.4,
-      11.4,
-      10.7,
-      10.1,
-      9.5,
-      9.1,
-      8.8,
-      8.5,
-      8.2,
-      8,
-      7.8,
-      7.4,
-      7,
-      7.2,
-      8.2,
-      9.6,
-      11.2,
-      12.7,
-      14.3,
-      15.5,
-      16.2,
-      16.4,
-      16.1,
-      15.3,
-      14,
-      12.7,
-      11.7,
-      10.7,
-      9.9,
-      9.4,
-      9.1,
-      8.9,
-      8.7,
-      8.5,
-      8.3,
-      8.1,
-      7.9,
-      7.7,
-      10.8,
-      12.7,
-      14.5,
-      16.4,
-      18.3,
-      19.7,
-      20.4,
-      20.6,
-      20.3,
-      19.5,
-      18.2,
-      16.7,
-      14.8,
-      12.7,
-      10.9,
-      9.9,
-      9.3,
-      8.7,
-      8.2,
-      7.8,
-      7.5,
-      7.2,
-      7,
-      6.9,
-      7.1,
-      7.5,
-      7.8,
-      7.8,
-      7.6,
-      7.5,
-      7.4,
-      7.4,
-      7.4,
-      7.2,
-      6.9,
-      6.8,
-      6.8,
-      7,
-      7.1,
-      7.1
-    ]
-  }
-}
-    """.trimIndent()
-
-    @Test
-    fun test() {
-        val weatherForecast = objectMapper.readValue(response, WeatherForecast::class.java)
-        val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
-        val weatherMap = mutableMapOf<LocalDate, MutableList<Double>>()
-
-        var i = 0
-        weatherForecast.hourly.time.forEach {
-            val temperature = weatherForecast.hourly.temperature2m[i++]
-            val key = LocalDate.parse(it, dateTimeFormatter)
-            weatherMap.compute(key) { _, value ->
-                value?.plus(temperature)?.toMutableList() ?: mutableListOf(temperature)
-            }
-        }
-        weatherMap.forEach {
-            println("${it.key} - ${it.value.average()}")
-        }
-    }
-}
\ No newline at end of file
diff --git a/app/src/test/kotlin/openmeteo/WeatherServiceTest.kt b/app/src/test/kotlin/openmeteo/WeatherServiceTest.kt
new file mode 100644
index 0000000..da16702
--- /dev/null
+++ b/app/src/test/kotlin/openmeteo/WeatherServiceTest.kt
@@ -0,0 +1,133 @@
+package openmeteo
+
+import hu.vanio.kotlin.feladat.ms.openmeteo.Hourly
+import hu.vanio.kotlin.feladat.ms.openmeteo.HourlyUnits
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherForecast
+import hu.vanio.kotlin.feladat.ms.openmeteo.WeatherService
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.function.Executable
+import java.time.LocalDate
+import java.time.Month
+import kotlin.test.assertEquals
+
+class WeatherServiceTest {
+    private val weatherForecastResponse = WeatherForecast(47.5,
+            19.0625,
+            0.030040740966796875,
+            3600,
+            "Europe/Budapest",
+            "CET",
+            124.0,
+            hourlyUnits = HourlyUnits("iso8601", "°C"),
+            hourly = Hourly(time = listOf("2024-03-18T00:00",
+                "2024-03-18T01:00",
+                "2024-03-18T02:00",
+                "2024-03-18T03:00",
+                "2024-03-18T04:00",
+                "2024-03-18T05:00",
+                "2024-03-18T06:00",
+                "2024-03-18T07:00",
+                "2024-03-18T08:00",
+                "2024-03-18T09:00",
+                "2024-03-18T10:00",
+                "2024-03-18T11:00",
+                "2024-03-18T12:00",
+                "2024-03-18T13:00",
+                "2024-03-18T14:00",
+                "2024-03-18T15:00",
+                "2024-03-18T16:00",
+                "2024-03-18T17:00",
+                "2024-03-18T18:00",
+                "2024-03-18T19:00",
+                "2024-03-18T20:00",
+                "2024-03-18T21:00",
+                "2024-03-18T22:00",
+                "2024-03-18T23:00",
+                "2024-03-19T00:00",
+                "2024-03-19T01:00",
+                "2024-03-19T02:00",
+                "2024-03-19T03:00",
+                "2024-03-19T04:00",
+                "2024-03-19T05:00",
+                "2024-03-19T06:00",
+                "2024-03-19T07:00",
+                "2024-03-19T08:00",
+                "2024-03-19T09:00",
+                "2024-03-19T10:00",
+                "2024-03-19T11:00",
+                "2024-03-19T12:00",
+                "2024-03-19T13:00",
+                "2024-03-19T14:00",
+                "2024-03-19T15:00",
+                "2024-03-19T16:00",
+                "2024-03-19T17:00",
+                "2024-03-19T18:00",
+                "2024-03-19T19:00",
+                "2024-03-19T20:00",
+                "2024-03-19T21:00",
+                "2024-03-19T22:00",
+                "2024-03-19T23:00"),
+                    temperature2m = listOf(6.0,
+                            5.7,
+                            5.4,
+                            5.2,
+                            4.8,
+                            4.6,
+                            4.5,
+                            4.7,
+                            5.9,
+                            7.3,
+                            8.9,
+                            9.7,
+                            10.4,
+                            10.9,
+                            11.1,
+                            11.0,
+                            10.6,
+                            9.8,
+                            8.7,
+                            7.4,
+                            6.2,
+                            5.4,
+                            4.7,
+                            4.2,
+                            3.7,
+                            3.3,
+                            3.0,
+                            2.9,
+                            2.8,
+                            2.8,
+                            2.8,
+                            3.2,
+                            3.9,
+                            4.7,
+                            5.5,
+                            6.4,
+                            7.4,
+                            8.0,
+                            8.5,
+                            8.8,
+                            8.8,
+                            8.6,
+                            7.8,
+                            6.7,
+                            5.6,
+                            4.7,
+                            4.0,
+                            3.4))
+            )
+
+    @Test
+    fun statistics() {
+        val weatherForecasts = WeatherService.statistics(weatherForecastResponse)
+
+        Assertions.assertAll (
+            Executable { assertEquals(2, weatherForecasts.weatherDailyForecast.size) } ,
+            Executable { assertEquals(LocalDate.of(2024, Month.MARCH, 18), weatherForecasts.weatherDailyForecast[0].day) },
+            Executable { assertEquals(7.21, weatherForecasts.weatherDailyForecast[0].average()) },
+            Executable { assertEquals(LocalDate.of(2024, Month.MARCH, 19), weatherForecasts.weatherDailyForecast[1].day) },
+            Executable { assertEquals(5.3, weatherForecasts.weatherDailyForecast[1].average()) }
+        )
+    }
+}
\ No newline at end of file