diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1182c84..e7e913f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ plugins { kotlin("jvm") version kotlinVersion kotlin("plugin.spring") version kotlinVersion id("org.springframework.boot") version "3.2.3" + kotlin("plugin.serialization") version "1.5.31" } group = "hu.kotlin.feladat.ms" @@ -15,8 +16,20 @@ 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("io.ktor:ktor-client-okhttp:1.6.6") + implementation("io.ktor:ktor-client-core:1.6.6") + implementation("io.ktor:ktor-client-json:1.6.6") + implementation("io.ktor:ktor-client-serialization:1.6.6") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign:3.1.3") // Spring Cloud OpenFeign integration + implementation("io.github.openfeign:feign-okhttp:11.2") + implementation("io.github.openfeign:feign-jackson:11.2") + implementation("org.reactivestreams:reactive-streams:1.0.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.6.0") + testImplementation("org.springframework.boot:spring-boot-starter-test:3.2.3") testImplementation(kotlin("test")) testImplementation("io.mockk:mockk:1.4.1") + testImplementation("com.github.tomakehurst:wiremock-standalone:3.0.1") } tasks.test { @@ -25,4 +38,4 @@ tasks.test { kotlin { jvmToolchain(17) -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/configuration/FeignConfig.kt b/app/src/main/kotlin/configuration/FeignConfig.kt new file mode 100644 index 0000000..b3693a0 --- /dev/null +++ b/app/src/main/kotlin/configuration/FeignConfig.kt @@ -0,0 +1,24 @@ +package hu.vanio.kotlin.feladat.ms.configuration + +import feign.Feign +import feign.jackson.JacksonDecoder +import feign.jackson.JacksonEncoder +import feign.okhttp.OkHttpClient +import hu.vanio.kotlin.feladat.ms.httpclient.WeatherFeignClient +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class FeignConfig { + @Value("\${base.url.weather.api}") + private lateinit var weatherApiBaseUrl: String + @Bean + fun weatherFeignClient(): WeatherFeignClient { + return Feign.builder() + .client(OkHttpClient()) + .encoder(JacksonEncoder()) + .decoder(JacksonDecoder()) + .target(WeatherFeignClient::class.java, weatherApiBaseUrl) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/controller/WeatherController.kt b/app/src/main/kotlin/controller/WeatherController.kt new file mode 100644 index 0000000..6ec6aae --- /dev/null +++ b/app/src/main/kotlin/controller/WeatherController.kt @@ -0,0 +1,33 @@ +package hu.vanio.kotlin.feladat.ms.controller + +import hu.vanio.kotlin.feladat.ms.data.DailyAverageData +import hu.vanio.kotlin.feladat.ms.data.DailyTempDataContainer +import hu.vanio.kotlin.feladat.ms.service.WeatherService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class WeatherController(private val weatherService: WeatherService) { + @GetMapping("/weather") + suspend fun getWeeklyTempData(): ResponseEntity { + return ResponseEntity.ok(weatherService.getDailyTempData()) + } + + @GetMapping("/average") + suspend fun getAverageTemps(): ResponseEntity> { + return ResponseEntity.ok(weatherService.getDailyAverageTemp()) + } + + @ExceptionHandler(IllegalArgumentException::class) + fun handleIllegalArgumentException(e: IllegalArgumentException): ResponseEntity<*> { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid request: ${e.message}") + } + + @ExceptionHandler(Exception::class) + fun handleException(e: Exception): ResponseEntity<*> { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred: ${e.message}") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/data/DailyAverageData.kt b/app/src/main/kotlin/data/DailyAverageData.kt new file mode 100644 index 0000000..f7604cc --- /dev/null +++ b/app/src/main/kotlin/data/DailyAverageData.kt @@ -0,0 +1,5 @@ +package hu.vanio.kotlin.feladat.ms.data + +import java.time.LocalDate + +data class DailyAverageData(var date: LocalDate, var average_temp: Double) \ No newline at end of file diff --git a/app/src/main/kotlin/data/DailyTempData.kt b/app/src/main/kotlin/data/DailyTempData.kt new file mode 100644 index 0000000..f3de742 --- /dev/null +++ b/app/src/main/kotlin/data/DailyTempData.kt @@ -0,0 +1,6 @@ +package hu.vanio.kotlin.feladat.ms.data + +import java.time.LocalDate +import java.time.LocalTime + +data class DailyTempData(var date: LocalDate, val hourlyTempData: Map) diff --git a/app/src/main/kotlin/data/DailyTempDataContainer.kt b/app/src/main/kotlin/data/DailyTempDataContainer.kt new file mode 100644 index 0000000..e84bd7a --- /dev/null +++ b/app/src/main/kotlin/data/DailyTempDataContainer.kt @@ -0,0 +1,5 @@ +package hu.vanio.kotlin.feladat.ms.data + +import java.time.LocalDate + +data class DailyTempDataContainer(var from: LocalDate?, var to: LocalDate?, val dailyTempData: List) \ No newline at end of file diff --git a/app/src/main/kotlin/data/HourlyData.kt b/app/src/main/kotlin/data/HourlyData.kt new file mode 100644 index 0000000..1e53aef --- /dev/null +++ b/app/src/main/kotlin/data/HourlyData.kt @@ -0,0 +1,12 @@ +package hu.vanio.kotlin.feladat.ms.data + +import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.Serializable + +@Serializable +data class HourlyData( + @JsonProperty("time") + val time: List?, + @JsonProperty("temperature_2m") + val temperature_2m: List? +) diff --git a/app/src/main/kotlin/data/HourlyUnits.kt b/app/src/main/kotlin/data/HourlyUnits.kt new file mode 100644 index 0000000..e3e6944 --- /dev/null +++ b/app/src/main/kotlin/data/HourlyUnits.kt @@ -0,0 +1,11 @@ +package hu.vanio.kotlin.feladat.ms.data + +import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.Serializable + +@Serializable +data class HourlyUnits( + @JsonProperty("time") val time: String, + @JsonProperty("temperature_2m") val temperature_2m: String +) { +} \ No newline at end of file diff --git a/app/src/main/kotlin/data/WeatherResponse.kt b/app/src/main/kotlin/data/WeatherResponse.kt new file mode 100644 index 0000000..79fea57 --- /dev/null +++ b/app/src/main/kotlin/data/WeatherResponse.kt @@ -0,0 +1,26 @@ +package hu.vanio.kotlin.feladat.ms.data + +import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.Serializable + +@Serializable +data class WeatherResponse( + @JsonProperty("latitude") + val latitude: Double, + @JsonProperty("longitude") + val longitude: Double, + @JsonProperty("generationtime_ms") + val generationtime_ms: Double, + @JsonProperty("utc_offset_seconds") + val utc_offset_seconds: Int, + @JsonProperty("timezone") + val timezone: String, + @JsonProperty("timezone_abbreviation") + val timezone_abbreviation: String, + @JsonProperty("elevation") + val elevation: Double, + @JsonProperty("hourly_units") + val hourly_units: HourlyUnits, + @JsonProperty("hourly") + val hourly: HourlyData +) diff --git a/app/src/main/kotlin/httpclient/WeatherFeignClient.kt b/app/src/main/kotlin/httpclient/WeatherFeignClient.kt new file mode 100644 index 0000000..a3f0a5d --- /dev/null +++ b/app/src/main/kotlin/httpclient/WeatherFeignClient.kt @@ -0,0 +1,11 @@ +package hu.vanio.kotlin.feladat.ms.httpclient + +import feign.RequestLine +import hu.vanio.kotlin.feladat.ms.data.WeatherResponse +import org.springframework.cloud.openfeign.FeignClient + +@FeignClient(name = "weatherFeignClient") +interface WeatherFeignClient { + @RequestLine("GET /forecast?latitude=47.4984&longitude=19.0404&hourly=temperature_2m&timezone=auto") + fun getWeather(): WeatherResponse +} \ No newline at end of file diff --git a/app/src/main/kotlin/parser/WeatherResponseParser.kt b/app/src/main/kotlin/parser/WeatherResponseParser.kt new file mode 100644 index 0000000..2274ab4 --- /dev/null +++ b/app/src/main/kotlin/parser/WeatherResponseParser.kt @@ -0,0 +1,68 @@ +package hu.vanio.kotlin.feladat.ms.parser + +import hu.vanio.kotlin.feladat.ms.data.DailyTempData +import hu.vanio.kotlin.feladat.ms.data.HourlyData +import hu.vanio.kotlin.feladat.ms.data.DailyTempDataContainer +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Service +class WeatherResponseParser { + + @Throws(IllegalArgumentException::class) + fun groupToDailyData(hourlyData: HourlyData): DailyTempDataContainer { + val (time, temp) = validateData(hourlyData) + val dailyTempData = groupByDays(time, temp) + + return dailyTempDataContainer(dailyTempData) + } + + private fun groupByDays(time: List, temp: List): List { + val dailyTempDataList = mutableListOf() + val days = time.map { LocalDate.parse(it.substring(0, 10)) }.distinct() + for (day in days) { + val hourlyTempsForDay = time.zip(temp) + .filter { it.first.toDate() == day } + .map { it.first.toTime() to it.second } + .toMap() + + dailyTempDataList.add(DailyTempData(day, hourlyTempsForDay)) + } + return dailyTempDataList + } + + private fun validateData(hourlyData: HourlyData): Pair, List> { + val time = hourlyData.time + val temp = hourlyData.temperature_2m + + if (time.isNullOrEmpty()) { + throw IllegalArgumentException("No time data available") + } + + if (temp.isNullOrEmpty()) { + throw IllegalArgumentException("No temp data available") + } + + return time to temp + } + + private fun dailyTempDataContainer(dailyTempDataList: List): DailyTempDataContainer { + val from = dailyTempDataList.first().date + val to = dailyTempDataList.last().date + return DailyTempDataContainer(from, to, dailyTempDataList) + } + + private fun String.toDate(): LocalDate { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm") + return LocalDate.parse(this, formatter) + } + + private fun String.toTime(): LocalTime { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm") + val dateTime = LocalDateTime.parse(this, formatter) + return dateTime.toLocalTime() + } +} diff --git a/app/src/main/kotlin/service/TempCalculator.kt b/app/src/main/kotlin/service/TempCalculator.kt new file mode 100644 index 0000000..3339b8c --- /dev/null +++ b/app/src/main/kotlin/service/TempCalculator.kt @@ -0,0 +1,18 @@ +package hu.vanio.kotlin.feladat.ms.service + +import hu.vanio.kotlin.feladat.ms.data.DailyTempData +import org.springframework.stereotype.Service + +@Service +class TempCalculator { + fun getAverageDailyTemp(dailyTempData: DailyTempData): Double { + if (dailyTempData.hourlyTempData.isEmpty()){ + throw IllegalArgumentException("Hourly temperature data for ${dailyTempData.date} is empty.") + } + + return dailyTempData.hourlyTempData.values.average() + } + + +} + diff --git a/app/src/main/kotlin/service/WeatherService.kt b/app/src/main/kotlin/service/WeatherService.kt new file mode 100644 index 0000000..ebe4159 --- /dev/null +++ b/app/src/main/kotlin/service/WeatherService.kt @@ -0,0 +1,35 @@ +package hu.vanio.kotlin.feladat.ms.service + +import hu.vanio.kotlin.feladat.ms.data.DailyAverageData +import hu.vanio.kotlin.feladat.ms.data.DailyTempDataContainer +import hu.vanio.kotlin.feladat.ms.httpclient.WeatherFeignClient +import hu.vanio.kotlin.feladat.ms.parser.WeatherResponseParser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.stereotype.Service + +@Service +class WeatherService( + private val weatherFeignClient: WeatherFeignClient, + private val weatherResponseParser: WeatherResponseParser, + private val tempCalculator: TempCalculator +) { + @Throws(IllegalArgumentException::class) + suspend fun getDailyTempData(): DailyTempDataContainer { + val response = withContext(Dispatchers.IO) { + weatherFeignClient.getWeather() + } + return weatherResponseParser.groupToDailyData(response.hourly) + } + + @Throws(IllegalStateException::class) + suspend fun getDailyAverageTemp(): List { + val dailyAverageTempList = mutableListOf() + val weeklyTempData = getDailyTempData() + for (dailyTempData in weeklyTempData.dailyTempData) { + val average = tempCalculator.getAverageDailyTemp(dailyTempData); + dailyAverageTempList.add(DailyAverageData(dailyTempData.date, average)) + } + return dailyAverageTempList + } +} \ 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..69659a6 --- /dev/null +++ b/app/src/main/resources/application.properties @@ -0,0 +1,6 @@ +logging.level.org.springframework.context.annotation = DEBUG + +server.tomcat.accesslog.enabled=true +server.tomcat.accesslog.pattern='%h %l %u %t "%r" %s %b' + +base.url.weather.api=https://api.open-meteo.com/v1 \ 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/it/WeatherFeignClientIT.kt b/app/src/test/kotlin/it/WeatherFeignClientIT.kt new file mode 100644 index 0000000..d275c0b --- /dev/null +++ b/app/src/test/kotlin/it/WeatherFeignClientIT.kt @@ -0,0 +1,54 @@ +package hu.vanio.kotlin.feladat.ms.it + +import com.github.tomakehurst.wiremock.WireMockServer +import hu.vanio.kotlin.feladat.ms.httpclient.WeatherFeignClient +import hu.vanio.kotlin.feladat.ms.mock.WeatherAPIMock.Companion.setupMockWeatherAPIResponse +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension +import kotlin.test.assertEquals + +@SpringBootTest +@ActiveProfiles("test") +@EnableFeignClients +@EnableConfigurationProperties +@ExtendWith(SpringExtension::class) +class WeatherFeignClientIT { + companion object { + private lateinit var wireMockServer: WireMockServer + + @BeforeAll + @JvmStatic + fun setUp() { + wireMockServer = WireMockServer(1040) + wireMockServer.start() + setupMockWeatherAPIResponse(wireMockServer) + } + + @AfterAll + @JvmStatic + fun tearDown() { + wireMockServer.stop() + } + } + + @Autowired + private lateinit var weatherFeignClient: WeatherFeignClient + + @Test + fun `test weather feign client get weather`() { + val weatherResponse = weatherFeignClient.getWeather() + + assertEquals(47.5, weatherResponse.latitude) + assertEquals(19.0625, weatherResponse.longitude) + assertEquals(168, weatherResponse.hourly.time?.size) + assertEquals(168, weatherResponse.hourly.temperature_2m?.size) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/mock/WeatherAPIMock.kt b/app/src/test/kotlin/mock/WeatherAPIMock.kt new file mode 100644 index 0000000..7f148b1 --- /dev/null +++ b/app/src/test/kotlin/mock/WeatherAPIMock.kt @@ -0,0 +1,32 @@ +package hu.vanio.kotlin.feladat.ms.mock + +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.util.StreamUtils.copyToString +import java.io.IOException +import java.nio.charset.Charset + +class WeatherAPIMock { + companion object { + @JvmStatic + @Throws(IOException::class) + fun setupMockWeatherAPIResponse(mockServer: WireMockServer) { + mockServer.stubFor( + WireMock.get(WireMock.urlPathMatching("/v1/forecast.*")) + .willReturn( + WireMock.aResponse() + .withStatus(HttpStatus.OK.value()) + .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .withBody( + copyToString( + WeatherAPIMock::class.java.classLoader.getResourceAsStream("payload/weather-forecast-response.json"), + Charset.defaultCharset() + ) + ) + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/parser/WeatherResponseParserTest.kt b/app/src/test/kotlin/parser/WeatherResponseParserTest.kt new file mode 100644 index 0000000..214e83b --- /dev/null +++ b/app/src/test/kotlin/parser/WeatherResponseParserTest.kt @@ -0,0 +1,105 @@ +package parser + +import hu.vanio.kotlin.feladat.ms.data.DailyTempData +import hu.vanio.kotlin.feladat.ms.data.HourlyData +import hu.vanio.kotlin.feladat.ms.data.DailyTempDataContainer +import hu.vanio.kotlin.feladat.ms.parser.WeatherResponseParser +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.LocalTime + +class WeatherResponseParserTest { + private val parser = WeatherResponseParser() + + @Test + fun `test groupToDailyData with null hourly time`() { + val hourlyData = HourlyData(null, temp()) + + val exception = assertThrows(IllegalArgumentException::class.java) { + parser.groupToDailyData(hourlyData) + } + + assertEquals("No time data available", exception.message) + } + + @Test + fun `test groupToDailyData with null hourly temp`() { + val hourlyData = HourlyData(time(), null) + + val exception = assertThrows(IllegalArgumentException::class.java) { + parser.groupToDailyData(hourlyData) + } + + assertEquals("No temp data available", exception.message) + } + + @Test + fun `test groupToDailyData when time data is empty`() { + val hourlyData = HourlyData(emptyList(), temp()) + + val exception = assertThrows(IllegalArgumentException::class.java) { + parser.groupToDailyData(hourlyData) + } + + assertEquals("No time data available", exception.message) + } + + @Test + fun `test groupToDailyData when temp data is empty`() { + val hourlyData = HourlyData(time(), emptyList()) + + val exception = assertThrows(IllegalArgumentException::class.java) { + parser.groupToDailyData(hourlyData) + } + + assertEquals("No temp data available", exception.message) + } + + @Test + fun `test groupToDailyData with valid data`() { + val hourlyData = HourlyData(time(), temp()) + + val result = parser.groupToDailyData(hourlyData) + + assertEquals(DailyTempDataContainer::class.java, result.javaClass) + assertEquals(LocalDate.parse("2024-03-19"), result.from) + assertEquals(LocalDate.parse("2024-03-20"), result.to) + assertEquals(2, result.dailyTempData.size) + + assertEquals(expectedFirstDayData().hourlyTempData, result.dailyTempData[0].hourlyTempData) + assertEquals(expectedSecondDayData().hourlyTempData, result.dailyTempData[1].hourlyTempData) + } + + private fun time(): List { + val time = mutableListOf() + time.add("2024-03-19T00:00") + time.add("2024-03-19T01:00") + time.add("2024-03-19T02:00") + time.add("2024-03-20T10:00") + time.add("2024-03-20T11:00") + + return time + } + + private fun expectedFirstDayData(): DailyTempData { + val expectedHourlyTempData = mutableMapOf() + expectedHourlyTempData[LocalTime.of(0,0)] = 0.0 + expectedHourlyTempData[LocalTime.of(1,0)] = 1.0 + expectedHourlyTempData[LocalTime.of(2,0)] = 2.0 + + return DailyTempData(LocalDate.of(2024, 3, 19), expectedHourlyTempData); + } + + private fun expectedSecondDayData(): DailyTempData { + val expectedHourlyTempData = mutableMapOf() + expectedHourlyTempData[LocalTime.of(10,0)] = 3.0 + expectedHourlyTempData[LocalTime.of(11,0)] = 4.0 + + return DailyTempData(LocalDate.of(2024, 3, 20), expectedHourlyTempData); + } + + private fun temp(): List { + return List(5) { it.toDouble() } + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/service/TempCalculatorTest.kt b/app/src/test/kotlin/service/TempCalculatorTest.kt new file mode 100644 index 0000000..0dd6c6e --- /dev/null +++ b/app/src/test/kotlin/service/TempCalculatorTest.kt @@ -0,0 +1,46 @@ +package service + +import hu.vanio.kotlin.feladat.ms.data.DailyTempData +import hu.vanio.kotlin.feladat.ms.service.TempCalculator +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.LocalTime + +class TempCalculatorTest { + private val calculator = TempCalculator() + + @Test + fun `test getAverage`() { + val dailyTempData = dailyTempData() + + val expectedAverage = (10.0 + 15.0 + 20.0) / 3 + + val result = calculator.getAverageDailyTemp(dailyTempData) + + assertEquals(expectedAverage, result) + } + + @Test + fun `test getAverage with empty data`() { + val dailyTempData = DailyTempData( + LocalDate.of(2024, 3, 19), + hourlyTempData = emptyMap() + ) + + val exception = assertThrows(IllegalArgumentException::class.java) { + calculator.getAverageDailyTemp(dailyTempData) + } + + assertEquals("Hourly temperature data for 2024-03-19 is empty.", exception.message) + } + + private fun dailyTempData(): DailyTempData { + val expectedHourlyTempData = mutableMapOf() + expectedHourlyTempData[LocalTime.of(0,0)] = 10.0 + expectedHourlyTempData[LocalTime.of(1,0)] = 15.0 + expectedHourlyTempData[LocalTime.of(2,0)] = 20.0 + + return DailyTempData(LocalDate.of(2024, 3, 19), expectedHourlyTempData); + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/service/WeatherServiceIT.kt b/app/src/test/kotlin/service/WeatherServiceIT.kt new file mode 100644 index 0000000..2c1f879 --- /dev/null +++ b/app/src/test/kotlin/service/WeatherServiceIT.kt @@ -0,0 +1,89 @@ +package hu.vanio.kotlin.feladat.ms.service + +import com.github.tomakehurst.wiremock.WireMockServer +import hu.vanio.kotlin.feladat.ms.mock.WeatherAPIMock.Companion.setupMockWeatherAPIResponse +import hu.vanio.kotlin.feladat.ms.parser.WeatherResponseParser +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.Import +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.time.LocalDate +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@SpringBootTest +@ActiveProfiles("test") +@EnableFeignClients +@EnableConfigurationProperties +@ExtendWith(SpringExtension::class) +@Import( + WeatherService::class, + WeatherResponseParser::class, + TempCalculator::class +) +class WeatherServiceIT { + companion object { + private lateinit var wireMockServer: WireMockServer + + @BeforeAll + @JvmStatic + fun setUp() { + wireMockServer = WireMockServer(1040) + wireMockServer.start() + setupMockWeatherAPIResponse(wireMockServer) + } + + @AfterAll + @JvmStatic + fun tearDown() { + wireMockServer.stop() + } + } + + @Autowired + private lateinit var weatherService: WeatherService + + @Test + fun `test get weekly temp data`() { + runBlocking { + val weeklyTempData = weatherService.getDailyTempData() + assertNotNull(weeklyTempData) + } + } + + @Test + fun `test get daily average temp for one week`() { + runBlocking { + val dailyAverageTempForOneWeek = weatherService.getDailyAverageTemp() + assertNotNull(dailyAverageTempForOneWeek) + assertEquals(LocalDate.parse("2024-03-20"), dailyAverageTempForOneWeek[0].date) + assertEquals(7.137499999999999, dailyAverageTempForOneWeek[0].average_temp) + + assertEquals(LocalDate.parse("2024-03-21"), dailyAverageTempForOneWeek[1].date) + assertEquals(9.845833333333333, dailyAverageTempForOneWeek[1].average_temp) + + assertEquals(LocalDate.parse("2024-03-22"), dailyAverageTempForOneWeek[2].date) + assertEquals(12.1625, dailyAverageTempForOneWeek[2].average_temp) + + assertEquals(LocalDate.parse("2024-03-23"), dailyAverageTempForOneWeek[3].date) + assertEquals(11.65, dailyAverageTempForOneWeek[3].average_temp) + + assertEquals(LocalDate.parse("2024-03-24"), dailyAverageTempForOneWeek[4].date) + assertEquals(8.345833333333335, dailyAverageTempForOneWeek[4].average_temp) + + assertEquals(LocalDate.parse("2024-03-25"), dailyAverageTempForOneWeek[5].date) + assertEquals(8.700000000000001, dailyAverageTempForOneWeek[5].average_temp) + + assertEquals(LocalDate.parse("2024-03-26"), dailyAverageTempForOneWeek[6].date) + assertEquals(9.183333333333334, dailyAverageTempForOneWeek[6].average_temp) + } + } +} \ No newline at end of file diff --git a/app/src/test/resources/application-test.properties b/app/src/test/resources/application-test.properties new file mode 100644 index 0000000..c71351f --- /dev/null +++ b/app/src/test/resources/application-test.properties @@ -0,0 +1 @@ +base.url.weather.api=http://localhost:1040/v1 \ No newline at end of file diff --git a/app/src/test/resources/payload/weather-forecast-response.json b/app/src/test/resources/payload/weather-forecast-response.json new file mode 100644 index 0000000..2934ec6 --- /dev/null +++ b/app/src/test/resources/payload/weather-forecast-response.json @@ -0,0 +1,355 @@ +{ + "latitude": 47.5, + "longitude": 19.0625, + "generationtime_ms": 0.02300739288330078, + "utc_offset_seconds": 3600, + "timezone": "Europe/Budapest", + "timezone_abbreviation": "CET", + "elevation": 124.0, + "hourly_units": { + "time": "iso8601", + "temperature_2m": "°C" + }, + "hourly": { + "time": [ + "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", + "2024-03-25T00:00", + "2024-03-25T01:00", + "2024-03-25T02:00", + "2024-03-25T03:00", + "2024-03-25T04:00", + "2024-03-25T05:00", + "2024-03-25T06:00", + "2024-03-25T07:00", + "2024-03-25T08:00", + "2024-03-25T09:00", + "2024-03-25T10:00", + "2024-03-25T11:00", + "2024-03-25T12:00", + "2024-03-25T13:00", + "2024-03-25T14:00", + "2024-03-25T15:00", + "2024-03-25T16:00", + "2024-03-25T17:00", + "2024-03-25T18:00", + "2024-03-25T19:00", + "2024-03-25T20:00", + "2024-03-25T21:00", + "2024-03-25T22:00", + "2024-03-25T23:00", + "2024-03-26T00:00", + "2024-03-26T01:00", + "2024-03-26T02:00", + "2024-03-26T03:00", + "2024-03-26T04:00", + "2024-03-26T05:00", + "2024-03-26T06:00", + "2024-03-26T07:00", + "2024-03-26T08:00", + "2024-03-26T09:00", + "2024-03-26T10:00", + "2024-03-26T11:00", + "2024-03-26T12:00", + "2024-03-26T13:00", + "2024-03-26T14:00", + "2024-03-26T15:00", + "2024-03-26T16:00", + "2024-03-26T17:00", + "2024-03-26T18:00", + "2024-03-26T19:00", + "2024-03-26T20:00", + "2024-03-26T21:00", + "2024-03-26T22:00", + "2024-03-26T23:00" + ], + "temperature_2m": [ + 3.4, + 3.0, + 2.6, + 2.2, + 1.6, + 1.4, + 1.2, + 1.7, + 3.4, + 5.6, + 9.1, + 10.9, + 12.0, + 12.5, + 12.9, + 13.1, + 13.6, + 12.9, + 11.6, + 10.0, + 8.3, + 7.0, + 6.1, + 5.2, + 4.9, + 4.9, + 4.8, + 4.6, + 4.1, + 3.9, + 3.8, + 4.5, + 6.5, + 9.4, + 11.7, + 13.3, + 14.4, + 15.3, + 15.8, + 16.1, + 16.0, + 15.4, + 13.8, + 12.2, + 11.1, + 10.2, + 9.7, + 9.9, + 9.8, + 9.6, + 9.3, + 8.8, + 9.1, + 9.3, + 9.1, + 9.5, + 11.0, + 12.7, + 14.1, + 15.2, + 16.0, + 16.2, + 16.4, + 16.4, + 16.0, + 15.3, + 14.0, + 12.6, + 11.6, + 10.7, + 9.8, + 9.4, + 9.0, + 8.2, + 8.0, + 7.7, + 7.3, + 7.2, + 7.4, + 8.1, + 9.4, + 11.0, + 13.1, + 14.6, + 15.8, + 17.0, + 17.4, + 17.6, + 17.5, + 17.2, + 16.4, + 12.4, + 11.0, + 9.7, + 8.6, + 8.0, + 7.7, + 7.4, + 7.0, + 6.7, + 6.5, + 6.2, + 6.0, + 6.2, + 7.1, + 8.5, + 9.6, + 10.3, + 10.9, + 11.2, + 11.4, + 11.3, + 11.0, + 10.4, + 9.5, + 8.7, + 7.8, + 6.9, + 6.2, + 5.8, + 5.5, + 5.3, + 4.9, + 4.5, + 4.3, + 4.1, + 4.2, + 4.8, + 6.5, + 8.8, + 10.7, + 11.9, + 12.8, + 13.4, + 13.6, + 13.8, + 13.5, + 12.5, + 11.1, + 9.9, + 9.1, + 8.3, + 7.8, + 7.5, + 7.3, + 7.0, + 6.5, + 5.8, + 5.4, + 5.4, + 5.6, + 6.1, + 7.0, + 8.1, + 9.1, + 9.9, + 10.7, + 11.4, + 12.3, + 13.0, + 13.4, + 13.2, + 12.6, + 11.8, + 11.0, + 10.0, + 9.2, + 8.6 + ] + } +} \ No newline at end of file