Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HF #3

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open

HF #3

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Build Gradle project

on:
push:

jobs:
build-gradle-project:
runs-on: ubuntu-latest
steps:
- name: Checkout project sources
uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'

- name: Setup Gradle
uses: gradle/gradle-build-action@v2

- name: Run build with Gradle Wrapper
run: ./gradlew build

- name: Run Kover
run: ./gradlew koverXmlReport

- name: Upload coverage reports to Codecov
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: dobrosi/kotlin-feladat-ms
files: ./build/reports/kover/xmlReport.xml
fail_ci_if_error: false
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![build](https://github.com/dobrosi/kotlin-feladat-ms/actions/workflows/ci.yml/badge.svg)](https://github.com/dobrosi/kotlin-feladat-ms/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/dobrosi/kotlin-feladat-ms/graph/badge.svg?token=Bv0alq4qqQ)](https://codecov.io/gh/dobrosi/kotlin-feladat-ms)
# Kotlin oktatáshoz házifeladat

## Készíteni kell egy nagyon egyszerű alkalmazást / programot, az alábbi funkcionalitással
Expand Down
11 changes: 10 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
id("org.springframework.boot") version "3.2.3"
id("org.jetbrains.kotlinx.kover") version "0.7.6"
}

group = "hu.kotlin.feladat.ms"
Expand All @@ -14,9 +15,17 @@ repositories {
dependencies {
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude("org.mockito:mockito-core")
}
testImplementation(kotlin("test"))
testImplementation("io.mockk:mockk:1.4.1")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.client

import hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.dto.Forecast
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient

@Component
class WeatherClient(@Autowired val forecast: WebClient.RequestHeadersSpec<*>) {
fun getForecast(): Forecast = forecast.retrieve().bodyToMono(Forecast::class.java).block()!!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.configuration

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient

@Configuration
class WeatherAppConfiguration {
@Bean
fun webClient(): WebClient = WebClient.builder().build()

@Bean
fun uriSpec(@Autowired webClient: WebClient): WebClient.RequestHeadersUriSpec<*> = webClient.get()

@Bean
fun forecast(
@Autowired uriSpec: WebClient.RequestHeadersUriSpec<*>,
@Value("\${forecast-url}") weatherApiUrl: String
): WebClient.RequestHeadersSpec<*> = uriSpec.uri(weatherApiUrl)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.controller

import hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.service.WeatherService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.servlet.ModelAndView

@Controller
class IndexController(@Autowired val weatherService: WeatherService) {
@GetMapping("/")
fun dailyAverages(): ModelAndView = modelAndView()
.addObject("dailyAverages", weatherService.getDailyAverages())

@ExceptionHandler(RuntimeException::class)
fun error(): ModelAndView = modelAndView()
.addObject("dailyAverages", emptyMap<Any, Any>())
.addObject("error", SERVICE_UNAVAILABLE)

private fun modelAndView() = ModelAndView("dailyAverages")
}

const val SERVICE_UNAVAILABLE = "Service unavailable"
12 changes: 12 additions & 0 deletions app/src/main/kotlin/hu/vanio/kotlin/feladat/ms/dto/Forecast.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.dto

import java.time.LocalDateTime

data class Forecast(
var hourly: Hourly
)

data class Hourly (
var time: Iterable<LocalDateTime>,
var temperature_2m: Iterable<Double>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.service

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.client.WeatherClient
import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.io.OutputStream

@Service
class WeatherService(@Autowired val weatherClient: WeatherClient) {
@PostConstruct
fun postConstruct() {
printDailyAverages()
}

fun printDailyAverages(out: OutputStream = System.out) = jacksonObjectMapper().writeValue(out, getDailyAverages())

fun getDailyAverages() = getHourly().groupBy { it.first.toLocalDate() }
.mapValues { it.value.map { pair -> pair.second }.average() }

fun getHourly() = weatherClient.getForecast().hourly.let {
it.time.zip(it.temperature_2m)
}
}
4 changes: 4 additions & 0 deletions app/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
weather:
api:
url: https://api.open-meteo.com/v1/
forecast-url: ${weather.api.url}/forecast?latitude=47.4984&longitude=19.0404&hourly=temperature_2m&timezone=auto
28 changes: 28 additions & 0 deletions app/src/main/resources/templates/dailyAverages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<title>Daily Temperature Averages</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style>
table, th, td {
border: 1px solid black;
}
.error {
color: red;
}
</style>
</head>
<body>
<p class="error" th:if="${error}">The weather service is currently unavailable.</p>
<table>
<tr>
<th>Date</th>
<th>Daily temperature average (&deg;C)</th>
</tr>
<tr th:each="dailyAverage : ${dailyAverages}">
<td th:text="${dailyAverage.key}"></td>
<td th:text="${dailyAverage.value}"></td>
</tr>
</table>
</body>
</html>
15 changes: 11 additions & 4 deletions app/src/test/kotlin/WeatherAppTest.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package hu.vanio.kotlin.feladat.ms

import kotlin.test.Test
import hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.service.WeatherService
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import kotlin.test.assertNotNull

@SpringBootTest
class WeatherAppTest {
@Autowired
lateinit var weatherService: WeatherService

@Test fun `sikeres lekerdezes`() {
TODO()
@Test
fun testContext() {
assertNotNull(weatherService)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.client

import hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.dto.Forecast
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
import kotlin.test.assertEquals

@ExtendWith(MockKExtension::class)
class WeatherClientTest{
@InjectMockKs
lateinit var weatherClient: WeatherClient

@MockK
lateinit var forecastRequest: WebClient.RequestHeadersSpec<*>

@MockK
lateinit var responseSpec: WebClient.ResponseSpec

@MockK
lateinit var forecastMono: Mono<Forecast>

@MockK
lateinit var forecast: Forecast

@Test
fun getForecast() {
every { forecastRequest.retrieve() } returns responseSpec
every { responseSpec.bodyToMono(Forecast::class.java) } returns forecastMono
every { forecastMono.block() } returns forecast

assertEquals(forecast, weatherClient.getForecast())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package hu.vanio.kotlin.feladat.ms.controller

import com.ninjasquad.springmockk.MockkBean
import hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.controller.IndexController
import hu.vanio.kotlin.feladat.ms.hu.vanio.kotlin.feladat.ms.service.WeatherService
import io.mockk.every
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.model
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import java.time.LocalDate

@SpringBootTest
class IndexControllerTest {

@MockkBean
lateinit var weatherService: WeatherService

lateinit var mockMvc: MockMvc

@BeforeEach fun init() {
mockMvc = MockMvcBuilders.standaloneSetup(IndexController(weatherService)).build()
}

@Test
fun `when call index page`() {
every { weatherService.getDailyAverages() } returns dailyAverages

mockMvc
.perform(get("/"))
.andExpect(status().isOk)
.andExpect(model().attribute("dailyAverages", dailyAverages))
}

@Test
fun `when call index page if service unavailable`() {
every { weatherService.getDailyAverages() } throws RuntimeException()

mockMvc
.perform(get("/"))
.andExpect(status().isOk)
.andExpect(model().attribute("dailyAverages", emptyMap<Any, Any>()))
.andExpect(model().attribute("error", "Service unavailable"))
}

}

val DATE: LocalDate = LocalDate.of(2024, 2, 21)
const val VALUE = 9.87
val dailyAverages = mapOf(DATE to VALUE)
Loading