diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 6eeb99f..147b6a7 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -19,6 +19,9 @@ java { } configurations { + compileOnly { + extendsFrom(configurations.annotationProcessor.get()) + } testImplementation { exclude(group = "org.mockito") // it is shipped with spring and there is no need, since we use mockk } @@ -35,41 +38,43 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-webflux") - implementation("io.github.oshai:kotlin-logging-jvm:5.1.0") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3") + implementation("io.github.oshai:kotlin-logging-jvm:5.1.0") + implementation("org.liquibase:liquibase-core:4.26.0") + implementation("org.postgresql:postgresql") - implementation ("org.postgresql:postgresql") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") val jjwtVersion = "0.12.5" implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion") runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion") runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion") - val kotestVersion = "5.8.0" testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") + + val kotestVersion = "5.8.0" testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testImplementation("io.kotest:kotest-property:$kotestVersion") - testImplementation("com.ninja-squad:springmockk:4.0.2") testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3") + testImplementation("com.ninja-squad:springmockk:4.0.2") } tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" jvmTarget = "17" + verbose = true } } -tasks.withType { - useJUnitPlatform() -} - tasks.test { + useJUnitPlatform() finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run } diff --git a/backend/src/main/kotlin/com/tul/backend/auth/base/config/SecurityConfiguration.kt b/backend/src/main/kotlin/com/tul/backend/auth/base/config/SecurityConfiguration.kt index b1cc74c..ef20648 100644 --- a/backend/src/main/kotlin/com/tul/backend/auth/base/config/SecurityConfiguration.kt +++ b/backend/src/main/kotlin/com/tul/backend/auth/base/config/SecurityConfiguration.kt @@ -35,7 +35,9 @@ class SecurityConfiguration( private val userUnsecuredEndpoints = arrayOf( "/api/auth/login", - "/api/auth/register" + "/api/auth/register", + "/api/weather/current/*", + "/api/weather/forecast/*", ) private val adminUnsecuredEndpoints = diff --git a/backend/src/main/kotlin/com/tul/backend/auth/base/service/CustomUserDetailsService.kt b/backend/src/main/kotlin/com/tul/backend/auth/base/service/CustomUserDetailsService.kt index a2dc33c..d4b4e9f 100644 --- a/backend/src/main/kotlin/com/tul/backend/auth/base/service/CustomUserDetailsService.kt +++ b/backend/src/main/kotlin/com/tul/backend/auth/base/service/CustomUserDetailsService.kt @@ -2,11 +2,11 @@ package com.tul.backend.auth.base.service import com.tul.backend.auth.entity.AuthUser import com.tul.backend.auth.repository.AuthUserRepository -import jakarta.transaction.Transactional import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service @Transactional diff --git a/backend/src/main/kotlin/com/tul/backend/auth/controller/AuthUserController.kt b/backend/src/main/kotlin/com/tul/backend/auth/controller/AuthUserController.kt index c9bc6e0..6a3547f 100644 --- a/backend/src/main/kotlin/com/tul/backend/auth/controller/AuthUserController.kt +++ b/backend/src/main/kotlin/com/tul/backend/auth/controller/AuthUserController.kt @@ -1,6 +1,5 @@ package com.tul.backend.auth.controller -import com.tul.backend.auth.dto.AuthUserDTO import com.tul.backend.auth.dto.LoginDTO import com.tul.backend.auth.dto.RegisterDTO import com.tul.backend.auth.service.AuthUserService @@ -34,9 +33,9 @@ class AuthUserController( @PostMapping("/auth/register") fun register( @RequestBody registerDTO: RegisterDTO, - ): ResponseEntity { + ): ResponseEntity { val response = authUserService.register(registerDTO) - val status = if (response != null) HttpStatus.OK else HttpStatus.BAD_REQUEST + val status = if (response) HttpStatus.OK else HttpStatus.BAD_REQUEST return ResponseEntity(response, status) } } diff --git a/backend/src/main/kotlin/com/tul/backend/auth/dto/AuthUserBase.kt b/backend/src/main/kotlin/com/tul/backend/auth/dto/AuthUserBase.kt deleted file mode 100644 index fccb5a1..0000000 --- a/backend/src/main/kotlin/com/tul/backend/auth/dto/AuthUserBase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tul.backend.auth.dto - -import com.tul.backend.auth.base.valueobject.EmailAddress - -interface AuthUserBase { - val email: EmailAddress - val password: String -} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/auth/dto/AuthUserDTO.kt b/backend/src/main/kotlin/com/tul/backend/auth/dto/AuthUserDTO.kt deleted file mode 100644 index d31ded3..0000000 --- a/backend/src/main/kotlin/com/tul/backend/auth/dto/AuthUserDTO.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tul.backend.auth.dto - -import com.tul.backend.auth.base.valueobject.EmailAddress -import com.tul.backend.auth.entity.AuthUser - -data class AuthUserDTO( - val id: Long, - val username: String, - val email: EmailAddress -) { - companion object { - fun from(authUser: AuthUser): AuthUserDTO { - return AuthUserDTO( - id = authUser.id, - username = authUser.username, - email = authUser.email, - ) - } - } -} diff --git a/backend/src/main/kotlin/com/tul/backend/auth/dto/LoginDTO.kt b/backend/src/main/kotlin/com/tul/backend/auth/dto/LoginDTO.kt index 84f47ee..d32268a 100644 --- a/backend/src/main/kotlin/com/tul/backend/auth/dto/LoginDTO.kt +++ b/backend/src/main/kotlin/com/tul/backend/auth/dto/LoginDTO.kt @@ -3,9 +3,9 @@ package com.tul.backend.auth.dto import com.tul.backend.auth.base.valueobject.EmailAddress data class LoginDTO( - override val email: EmailAddress, - override val password: String, -) : AuthUserBase { + val email: EmailAddress, + val password: String, +) { fun isValid(): Boolean { return email.isValid() && password.isNotEmpty() } diff --git a/backend/src/main/kotlin/com/tul/backend/auth/dto/TokenDTO.kt b/backend/src/main/kotlin/com/tul/backend/auth/dto/TokenDTO.kt deleted file mode 100644 index 01496c8..0000000 --- a/backend/src/main/kotlin/com/tul/backend/auth/dto/TokenDTO.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tul.backend.auth.dto - -data class TokenDTO( - val token: String -) diff --git a/backend/src/main/kotlin/com/tul/backend/auth/entity/AuthUser.kt b/backend/src/main/kotlin/com/tul/backend/auth/entity/AuthUser.kt index 7e0a8ba..3df4310 100644 --- a/backend/src/main/kotlin/com/tul/backend/auth/entity/AuthUser.kt +++ b/backend/src/main/kotlin/com/tul/backend/auth/entity/AuthUser.kt @@ -2,7 +2,6 @@ package com.tul.backend.auth.entity import com.tul.backend.auth.base.valueobject.AuthUserRole import com.tul.backend.auth.base.valueobject.EmailAddress -import com.tul.backend.auth.dto.AuthUserBase import com.tul.backend.auth.dto.RegisterDTO import jakarta.persistence.Entity import jakarta.persistence.EnumType @@ -17,11 +16,11 @@ class AuthUser( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L, val username: String, - override val email: EmailAddress, - override var password: String, + val email: EmailAddress, + var password: String, @Enumerated(EnumType.STRING) val role: AuthUserRole, -) : AuthUserBase { +) { companion object { fun from(registerDTO: RegisterDTO): AuthUser { return AuthUser( diff --git a/backend/src/main/kotlin/com/tul/backend/auth/service/AuthUserService.kt b/backend/src/main/kotlin/com/tul/backend/auth/service/AuthUserService.kt index 3045c6a..f39e348 100644 --- a/backend/src/main/kotlin/com/tul/backend/auth/service/AuthUserService.kt +++ b/backend/src/main/kotlin/com/tul/backend/auth/service/AuthUserService.kt @@ -1,14 +1,13 @@ package com.tul.backend.auth.service -import com.tul.backend.auth.dto.AuthUserDTO import com.tul.backend.auth.dto.LoginDTO import com.tul.backend.auth.dto.RegisterDTO import com.tul.backend.auth.repository.AuthUserRepository import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import jakarta.transaction.Transactional import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional private val log = KotlinLogging.logger {} @@ -27,21 +26,21 @@ class AuthUserService( return authenticationHandler.authenticate(loginDTO, request, response) } - fun register(registerDTO: RegisterDTO): AuthUserDTO? { + fun register(registerDTO: RegisterDTO): Boolean { if (!registerDTO.isValid()) { log.warn { "RegisterDTO: $registerDTO is invalid" } - return null + return false } val exists = authUserRepository.existsByEmail(registerDTO.email.value) if (exists) { log.warn { "User with email: ${registerDTO.email} already exists" } - return null + return false } - val authUser = authUserRepository.save( + authUserRepository.save( authenticationHandler.hashRegistrationPassword(registerDTO) ) - return AuthUserDTO.from(authUser) + return true } } diff --git a/backend/src/main/kotlin/com/tul/backend/auth/service/AuthenticationHandler.kt b/backend/src/main/kotlin/com/tul/backend/auth/service/AuthenticationHandler.kt index 219b642..af174e8 100644 --- a/backend/src/main/kotlin/com/tul/backend/auth/service/AuthenticationHandler.kt +++ b/backend/src/main/kotlin/com/tul/backend/auth/service/AuthenticationHandler.kt @@ -7,12 +7,12 @@ import com.tul.backend.auth.dto.RegisterDTO import com.tul.backend.auth.entity.AuthUser import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import jakarta.transaction.Transactional import org.springframework.beans.factory.annotation.Qualifier import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service @Transactional diff --git a/backend/src/main/kotlin/com/tul/backend/configuration/jackson/TrimmingStringDeserializer.kt b/backend/src/main/kotlin/com/tul/backend/shared/jackson/TrimmingStringDeserializer.kt similarity index 90% rename from backend/src/main/kotlin/com/tul/backend/configuration/jackson/TrimmingStringDeserializer.kt rename to backend/src/main/kotlin/com/tul/backend/shared/jackson/TrimmingStringDeserializer.kt index b1391a7..a0d7af5 100644 --- a/backend/src/main/kotlin/com/tul/backend/configuration/jackson/TrimmingStringDeserializer.kt +++ b/backend/src/main/kotlin/com/tul/backend/shared/jackson/TrimmingStringDeserializer.kt @@ -1,4 +1,4 @@ -package com.tul.backend.configuration.jackson +package com.tul.backend.shared.jackson import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext diff --git a/backend/src/main/kotlin/com/tul/backend/weather/controller/WeatherController.kt b/backend/src/main/kotlin/com/tul/backend/weather/controller/WeatherController.kt index b7c8b7b..fd80d04 100644 --- a/backend/src/main/kotlin/com/tul/backend/weather/controller/WeatherController.kt +++ b/backend/src/main/kotlin/com/tul/backend/weather/controller/WeatherController.kt @@ -1,9 +1,12 @@ package com.tul.backend.weather.controller +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.dto.ForecastWeatherDTO import com.tul.backend.weather.service.WeatherService import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -12,9 +15,20 @@ import org.springframework.web.bind.annotation.RestController class WeatherController( private val weatherService: WeatherService, ) { - @GetMapping("/auth/weather/actual-weather") - fun getActualWeather(): ResponseEntity { - val response = weatherService.getActualWeather() + @GetMapping("/weather/current/{location}") + fun getCurrentWeather( + @PathVariable location: String + ): ResponseEntity { + val response = weatherService.getCurrentWeather(location) + val status = if (response != null) HttpStatus.OK else HttpStatus.NOT_FOUND + return ResponseEntity(response, status) + } + + @GetMapping("/weather/forecast/{location}") + fun getForecastWeather( + @PathVariable location: String + ): ResponseEntity { + val response = weatherService.getForecastWeather(location) val status = if (response != null) HttpStatus.OK else HttpStatus.NOT_FOUND return ResponseEntity(response, status) } diff --git a/backend/src/main/kotlin/com/tul/backend/weather/dto/ActualWeatherDTO.kt b/backend/src/main/kotlin/com/tul/backend/weather/dto/ActualWeatherDTO.kt deleted file mode 100644 index 870ad07..0000000 --- a/backend/src/main/kotlin/com/tul/backend/weather/dto/ActualWeatherDTO.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tul.backend.weather.dto - -data class ActualWeatherDTO( - val temperature: Double, -) diff --git a/backend/src/main/kotlin/com/tul/backend/weather/dto/CurrentWeatherDTO.kt b/backend/src/main/kotlin/com/tul/backend/weather/dto/CurrentWeatherDTO.kt new file mode 100644 index 0000000..4071339 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/dto/CurrentWeatherDTO.kt @@ -0,0 +1,25 @@ +package com.tul.backend.weather.dto + +import com.tul.backend.weather.valueobject.CurrentWeatherJson +import java.time.LocalDateTime + +data class CurrentWeatherDTO( + val time: LocalDateTime, + val temperature: Double, + val cloudCover: Int, + val windSpeed: Double, + val isDay: Boolean +) { + + companion object { + fun from(currentWeatherJson: CurrentWeatherJson): CurrentWeatherDTO { + return CurrentWeatherDTO( + currentWeatherJson.current.time, + currentWeatherJson.current.temperature, + currentWeatherJson.current.cloudCover, + currentWeatherJson.current.windSpeed, + currentWeatherJson.current.isDay == 1 + ) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/dto/ForecastWeatherDTO.kt b/backend/src/main/kotlin/com/tul/backend/weather/dto/ForecastWeatherDTO.kt new file mode 100644 index 0000000..a3c2d31 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/dto/ForecastWeatherDTO.kt @@ -0,0 +1,23 @@ +package com.tul.backend.weather.dto + +import com.tul.backend.weather.valueobject.ForecastWeatherJson +import java.time.LocalDate + +data class ForecastWeatherDTO( + val time: List, + val maxTemperature: List, + val minTemperature: List, + val maxWindSpeed: List +) { + + companion object { + fun from(forecastWeatherJson: ForecastWeatherJson): ForecastWeatherDTO { + return ForecastWeatherDTO( + forecastWeatherJson.daily.time, + forecastWeatherJson.daily.maxTemperature, + forecastWeatherJson.daily.minTemperature, + forecastWeatherJson.daily.maxWindSpeed + ) + } + } +} diff --git a/backend/src/main/kotlin/com/tul/backend/weather/dto/LocationDTO.kt b/backend/src/main/kotlin/com/tul/backend/weather/dto/LocationDTO.kt new file mode 100644 index 0000000..bb87085 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/dto/LocationDTO.kt @@ -0,0 +1,18 @@ +package com.tul.backend.weather.dto + +import com.tul.backend.weather.valueobject.LocationOutcomeJson + +data class LocationDTO( + var latitude: Double, + var longitude: Double +) { + + companion object { + fun from(locationOutcomeJson: LocationOutcomeJson): LocationDTO { + return LocationDTO( + latitude = locationOutcomeJson.latitude, + longitude = locationOutcomeJson.longitude + ) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/WeatherHandler.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/WeatherHandler.kt new file mode 100644 index 0000000..acc6287 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/WeatherHandler.kt @@ -0,0 +1,41 @@ +package com.tul.backend.weather.service + +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.dto.ForecastWeatherDTO +import com.tul.backend.weather.service.extractor.WeatherExtractor +import com.tul.backend.weather.service.parser.WebClientParser +import com.tul.backend.weather.valueobject.WeatherStatus.CURRENT +import com.tul.backend.weather.valueobject.WeatherStatus.FORECAST +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Service + +private val log = KotlinLogging.logger {} + +@Service +class WeatherHandler( + weatherExtractors: List, + private val webClientParser: WebClientParser +) { + + private val weatherExtractorList = weatherExtractors.associateBy { it.weatherStatus } + + fun getCurrentWeather(location: String): CurrentWeatherDTO? { + val weatherExtractor = weatherExtractorList[CURRENT] + if (weatherExtractor == null) { + log.warn { "No weather extractor found for $CURRENT" } + return null + } + val weatherJson = weatherExtractor.getWeatherJson(location) ?: return null + return webClientParser.parseCurrentWeatherJson(weatherJson) + } + + fun getForecastWeather(location: String): ForecastWeatherDTO? { + val weatherExtractor = weatherExtractorList[FORECAST] + if (weatherExtractor == null) { + log.warn { "No weather extractor found for $FORECAST" } + return null + } + val weatherJson = weatherExtractor.getWeatherJson(location) ?: return null + return webClientParser.parseForecastWeatherJson(weatherJson) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/WeatherService.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/WeatherService.kt index 2ab123c..0f06840 100644 --- a/backend/src/main/kotlin/com/tul/backend/weather/service/WeatherService.kt +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/WeatherService.kt @@ -1,10 +1,31 @@ package com.tul.backend.weather.service +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.dto.ForecastWeatherDTO +import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.stereotype.Service +private val log = KotlinLogging.logger {} + @Service -class WeatherService { - fun getActualWeather(): String? { - return "ahoj" +class WeatherService( + private val weatherHandler: WeatherHandler +) { + fun getCurrentWeather(location: String): CurrentWeatherDTO? { + val currentWeatherDTO = weatherHandler.getCurrentWeather(location) + if (currentWeatherDTO == null) { + log.warn { "No current weather found for $location" } + return null + } + return currentWeatherDTO + } + + fun getForecastWeather(location: String): ForecastWeatherDTO? { + val forecastWeatherDTO = weatherHandler.getForecastWeather(location) + if (forecastWeatherDTO == null) { + log.warn { "No forecast weather found for $location" } + return null + } + return forecastWeatherDTO } } diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/client/GeoDecoderClientService.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/client/GeoDecoderClientService.kt new file mode 100644 index 0000000..c9b960a --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/client/GeoDecoderClientService.kt @@ -0,0 +1,48 @@ +package com.tul.backend.weather.service.client + +import com.tul.backend.weather.valueobject.WebClientOutcome +import org.springframework.http.HttpStatus +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClientResponseException +import org.springframework.web.reactive.function.client.awaitBody +import reactor.netty.http.client.HttpClient + +@Service +class GeoDecoderClientService( + webClientBuilder: WebClient.Builder +) { + + private val webClient = webClientBuilder + .clientConnector(ReactorClientHttpConnector(HttpClient.create())) + .codecs { configurer -> + configurer + .defaultCodecs() + .maxInMemorySize(4096 * 1024) // Listing detail html resource size is 2,6MB + }.baseUrl("https://geocoding-api.open-meteo.com/v1/search").build() + + // https://geocoding-api.open-meteo.com/v1/search?name=%C4%8Cesk%C3%A9+Bud%C4%9Bjovice&count=1&language=en&format=json + suspend fun getLocation(location: String): WebClientOutcome { + return try { + val response = webClient.get() + .uri { uriBuilder -> + uriBuilder + .queryParam("name", location) + .queryParam("count", "1") + .queryParam("language", "en") + .queryParam("format", "json") + .build() + } + .retrieve() + .awaitBody() + + WebClientOutcome.Success(response) + } catch (e: WebClientResponseException) { + when (e.statusCode) { + HttpStatus.SERVICE_UNAVAILABLE -> WebClientOutcome.Failure.ServerError + else -> WebClientOutcome.Failure.UnspecifiedError(e) + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/client/OpenMeteoClientService.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/client/OpenMeteoClientService.kt new file mode 100644 index 0000000..892cfb1 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/client/OpenMeteoClientService.kt @@ -0,0 +1,73 @@ +package com.tul.backend.weather.service.client + +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.valueobject.WebClientOutcome +import org.springframework.http.HttpStatus +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClientResponseException +import org.springframework.web.reactive.function.client.awaitBody +import reactor.netty.http.client.HttpClient + +@Service +class OpenMeteoClientService { + + private val webClient = WebClient.builder() + .clientConnector(ReactorClientHttpConnector(HttpClient.create())) + .codecs { configurer -> + configurer + .defaultCodecs() + .maxInMemorySize(4096 * 1024) // Listing detail html resource size is 2,6MB + }.baseUrl("https://api.open-meteo.com/v1/forecast").build() + + // https://api.open-meteo.com/v1/forecast?latitude=50.7671&longitude=15.0562&daily=temperature_2m_max,temperature_2m_min,wind_speed_10m_max,wind_gusts_10m_max&timezone=auto&past_days=7 + suspend fun getForecastWeather(locationDTO: LocationDTO): WebClientOutcome { + return try { + val response = webClient.get() + .uri { uriBuilder -> + uriBuilder + .queryParam("latitude", locationDTO.latitude) + .queryParam("longitude", locationDTO.longitude) + .queryParam("daily", "temperature_2m_max,temperature_2m_min,wind_speed_10m_max") + .queryParam("timezone", "auto") + .queryParam("past_days", 7) + .build() + } + .retrieve() + .awaitBody() + + WebClientOutcome.Success(response) + } catch (e: WebClientResponseException) { + when (e.statusCode) { + HttpStatus.SERVICE_UNAVAILABLE -> WebClientOutcome.Failure.ServerError + else -> WebClientOutcome.Failure.UnspecifiedError(e) + } + } + } + + // https://api.open-meteo.com/v1/forecast?latitude=50.7671&longitude=15.0562¤t=temperature_2m,is_day,cloud_cover,wind_speed_10m&timezone=auto&forecast_days=1 + suspend fun getCurrentWeather(locationDTO: LocationDTO): WebClientOutcome { + return try { + val response = webClient.get() + .uri { uriBuilder -> + uriBuilder + .queryParam("latitude", locationDTO.latitude) + .queryParam("longitude", locationDTO.longitude) + .queryParam("current", "temperature_2m,is_day,cloud_cover,wind_speed_10m") + .queryParam("timezone", "auto") + .queryParam("forecast_days", 1) + .build() + } + .retrieve() + .awaitBody() + + WebClientOutcome.Success(response) + } catch (e: WebClientResponseException) { + when (e.statusCode) { + HttpStatus.SERVICE_UNAVAILABLE -> WebClientOutcome.Failure.ServerError + else -> WebClientOutcome.Failure.UnspecifiedError(e) + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/CurrentWeatherExtractorImpl.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/CurrentWeatherExtractorImpl.kt new file mode 100644 index 0000000..429aa3a --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/CurrentWeatherExtractorImpl.kt @@ -0,0 +1,59 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.service.client.OpenMeteoClientService +import com.tul.backend.weather.valueobject.WeatherStatus +import com.tul.backend.weather.valueobject.WeatherStatus.CURRENT +import com.tul.backend.weather.valueobject.WebClientOutcome +import com.tul.backend.weather.valueobject.WebClientOutcome.Failure +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.runBlocking +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +private val log = KotlinLogging.logger {} + +@Service +@Transactional +class CurrentWeatherExtractorImpl( + private val locationExtractor: LocationExtractor, + private val openMeteoClientService: OpenMeteoClientService +) : WeatherExtractor { + + override val weatherStatus: WeatherStatus = CURRENT + + override fun getWeatherJson(location: String): String? { + return runBlocking { + val locationDTO = locationExtractor.getLocation(location) ?: return@runBlocking null + return@runBlocking when (val currentWeather = getCurrentWeather(locationDTO)) { + null -> null + else -> currentWeather + } + } + } + + suspend fun getCurrentWeather(locationDTO: LocationDTO): String? { + return when (val locationJson = openMeteoClientService.getCurrentWeather(locationDTO)) { + is Failure -> { + processWebClientResponse(locationJson) + null + } + + is WebClientOutcome.Success -> { + return locationJson.response + } + } + } + + suspend fun processWebClientResponse(failure: Failure) { + when (failure) { + is Failure.ServerError -> { + log.warn { "OpenMeteo clint threw serverError, while processing current weather" } + } + + is Failure.UnspecifiedError -> { + log.error(failure.exception) { "OpenMeteo client threw unspecified error, while processing current weather" } + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/ForecastWeatherExtractorImpl.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/ForecastWeatherExtractorImpl.kt new file mode 100644 index 0000000..99879e9 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/ForecastWeatherExtractorImpl.kt @@ -0,0 +1,60 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.service.client.OpenMeteoClientService +import com.tul.backend.weather.valueobject.WeatherStatus +import com.tul.backend.weather.valueobject.WeatherStatus.FORECAST +import com.tul.backend.weather.valueobject.WebClientOutcome +import com.tul.backend.weather.valueobject.WebClientOutcome.Failure +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.runBlocking +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +private val log = KotlinLogging.logger {} + +@Service +@Transactional +class ForecastWeatherExtractorImpl( + private val locationExtractor: LocationExtractor, + private val openMeteoClientService: OpenMeteoClientService +) : WeatherExtractor { + + override val weatherStatus: WeatherStatus = FORECAST + + override fun getWeatherJson(location: String): String? { + return runBlocking { + val locationDTO = locationExtractor.getLocation(location) ?: return@runBlocking null + return@runBlocking when (val currentWeather = getForecastWeather(locationDTO)) { + null -> null + else -> currentWeather + } + } + } + + suspend fun getForecastWeather(locationDTO: LocationDTO): String? { + return when (val locationJson = openMeteoClientService.getForecastWeather(locationDTO)) { + is Failure -> { + processWebClientResponse(locationJson) + null + } + + is WebClientOutcome.Success -> { + return locationJson.response + } + } + } + + suspend fun processWebClientResponse(failure: Failure) { + when (failure) { + is Failure.ServerError -> { + log.warn { "OpenMeteo clint threw serverError, while processing forecast weather" } + } + + is Failure.UnspecifiedError -> { + log.error(failure.exception) { "OpenMeteo client threw unspecified error, while processing forecast weather" } + } + } + } + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/LocationExtractor.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/LocationExtractor.kt new file mode 100644 index 0000000..730b1e5 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/LocationExtractor.kt @@ -0,0 +1,8 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.dto.LocationDTO + +interface LocationExtractor { + + suspend fun getLocation(location: String): LocationDTO? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/LocationExtractorImpl.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/LocationExtractorImpl.kt new file mode 100644 index 0000000..3d21b6d --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/LocationExtractorImpl.kt @@ -0,0 +1,46 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.service.client.GeoDecoderClientService +import com.tul.backend.weather.service.parser.WebClientParser +import com.tul.backend.weather.valueobject.WebClientOutcome +import com.tul.backend.weather.valueobject.WebClientOutcome.Failure +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +private val log = KotlinLogging.logger {} + +@Service +@Transactional +class LocationExtractorImpl( + private val geoDecoderClientService: GeoDecoderClientService, + private val webClientParser: WebClientParser +) : LocationExtractor { + + override suspend fun getLocation(location: String): LocationDTO? { + return when (val locationJson = geoDecoderClientService.getLocation(location)) { + is Failure -> { + processWebClientResponse(locationJson) + null + } + + is WebClientOutcome.Success -> { + return webClientParser.parseLocationJson(locationJson.response) + } + } + } + + + private suspend fun processWebClientResponse(failure: Failure) { + when (failure) { + is Failure.ServerError -> { + log.warn { "Geo clint threw serverError, while processing location" } + } + + is Failure.UnspecifiedError -> { + log.error(failure.exception) { "Geo client threw unspecified error, while processing location" } + } + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/WeatherExtractor.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/WeatherExtractor.kt new file mode 100644 index 0000000..bd9d328 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/extractor/WeatherExtractor.kt @@ -0,0 +1,10 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.valueobject.WeatherStatus + +interface WeatherExtractor { + + val weatherStatus: WeatherStatus + + fun getWeatherJson(location: String): String? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/service/parser/WebClientParser.kt b/backend/src/main/kotlin/com/tul/backend/weather/service/parser/WebClientParser.kt new file mode 100644 index 0000000..51f670f --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/service/parser/WebClientParser.kt @@ -0,0 +1,48 @@ +package com.tul.backend.weather.service.parser + +import com.fasterxml.jackson.databind.ObjectMapper +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.dto.ForecastWeatherDTO +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.valueobject.CurrentWeatherJson +import com.tul.backend.weather.valueobject.ForecastWeatherJson +import com.tul.backend.weather.valueobject.LocationOutcomeJsonList +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component + +private val log = KotlinLogging.logger {} + +@Component +class WebClientParser( + private val objectMapper: ObjectMapper +) { + fun parseLocationJson(json: String): LocationDTO? { + return try { + val response = objectMapper.readValue(json, LocationOutcomeJsonList::class.java).results + LocationDTO.from(response.first()) + } catch (e: Exception) { + log.error(e) { "Error parsing location json" } + null + } + } + + fun parseCurrentWeatherJson(json: String): CurrentWeatherDTO? { + return try { + val response = objectMapper.readValue(json, CurrentWeatherJson::class.java) + CurrentWeatherDTO.from(response) + } catch (e: Exception) { + log.error(e) { "Error parsing current weather json" } + null + } + } + + fun parseForecastWeatherJson(json: String): ForecastWeatherDTO? { + return try { + val response = objectMapper.readValue(json, ForecastWeatherJson::class.java) + ForecastWeatherDTO.from(response) + } catch (e: Exception) { + log.error(e) { "Error parsing forecast weather json" } + null + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/valueobject/CurrentWeatherJson.kt b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/CurrentWeatherJson.kt new file mode 100644 index 0000000..58c1582 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/CurrentWeatherJson.kt @@ -0,0 +1,22 @@ +package com.tul.backend.weather.valueobject + +import com.fasterxml.jackson.annotation.JsonProperty +import java.time.LocalDateTime + +data class CurrentWeatherJson( + @JsonProperty("current") + val current: Json +) + +data class Json( + @JsonProperty("time") + val time: LocalDateTime, + @JsonProperty("temperature_2m") + val temperature: Double, + @JsonProperty("cloud_cover") + val cloudCover: Int, + @JsonProperty("is_day") + val isDay: Int, + @JsonProperty("wind_speed_10") + val windSpeed: Double, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/valueobject/ForcastWeatherJson.kt b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/ForcastWeatherJson.kt new file mode 100644 index 0000000..641b00f --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/ForcastWeatherJson.kt @@ -0,0 +1,20 @@ +package com.tul.backend.weather.valueobject + +import com.fasterxml.jackson.annotation.JsonProperty +import java.time.LocalDate + +data class ForecastWeatherJson( + @JsonProperty("daily") + val daily: HourlyDataJson +) + +data class HourlyDataJson( + @JsonProperty("time") + val time: List, + @JsonProperty("temperature_2m_max") + val maxTemperature: List, + @JsonProperty("temperature_2m_min") + val minTemperature: List, + @JsonProperty("wind_speed_10m_max") + val maxWindSpeed: List, +) diff --git a/backend/src/main/kotlin/com/tul/backend/weather/valueobject/LocationOutcomeJson.kt b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/LocationOutcomeJson.kt new file mode 100644 index 0000000..91ded67 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/LocationOutcomeJson.kt @@ -0,0 +1,15 @@ +package com.tul.backend.weather.valueobject + +import com.fasterxml.jackson.annotation.JsonProperty + + +data class LocationOutcomeJsonList( + val results: List +) + +data class LocationOutcomeJson( + @JsonProperty("latitude") + val latitude: Double, + @JsonProperty("longitude") + val longitude: Double +) diff --git a/backend/src/main/kotlin/com/tul/backend/weather/valueobject/WeatherStatus.kt b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/WeatherStatus.kt new file mode 100644 index 0000000..7bae591 --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/WeatherStatus.kt @@ -0,0 +1,6 @@ +package com.tul.backend.weather.valueobject + +enum class WeatherStatus { + CURRENT, + FORECAST +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/tul/backend/weather/valueobject/WebClientOutcome.kt b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/WebClientOutcome.kt new file mode 100644 index 0000000..c0dd96b --- /dev/null +++ b/backend/src/main/kotlin/com/tul/backend/weather/valueobject/WebClientOutcome.kt @@ -0,0 +1,10 @@ +package com.tul.backend.weather.valueobject + +sealed class WebClientOutcome { + data class Success(val response: String) : WebClientOutcome() + + sealed class Failure : WebClientOutcome() { + data object ServerError : Failure() + data class UnspecifiedError(val exception: Exception) : Failure() + } +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/tul/backend/IntegrationTestConfig.kt b/backend/src/test/kotlin/com/tul/backend/IntegrationTestConfig.kt index 1b6aeac..e9c4c78 100644 --- a/backend/src/test/kotlin/com/tul/backend/IntegrationTestConfig.kt +++ b/backend/src/test/kotlin/com/tul/backend/IntegrationTestConfig.kt @@ -1,6 +1,12 @@ package com.tul.backend +import com.ninjasquad.springmockk.MockkBean import org.springframework.context.annotation.Configuration +import org.springframework.web.reactive.function.client.WebClient @Configuration -class IntegrationTestConfig +class IntegrationTestConfig { + + @MockkBean(relaxed = true) + lateinit var webClient: WebClient +} diff --git a/backend/src/test/kotlin/com/tul/backend/TestUtils.kt b/backend/src/test/kotlin/com/tul/backend/TestUtils.kt index 3209489..e0d76c8 100644 --- a/backend/src/test/kotlin/com/tul/backend/TestUtils.kt +++ b/backend/src/test/kotlin/com/tul/backend/TestUtils.kt @@ -1,8 +1,18 @@ package com.tul.backend +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.tul.backend.auth.base.valueobject.AuthUserRole import com.tul.backend.auth.base.valueobject.EmailAddress import com.tul.backend.auth.entity.AuthUser +import com.tul.backend.shared.jackson.TrimmingStringDeserializer + +val objectMapper: ObjectMapper = jacksonObjectMapper() + .registerModule(JavaTimeModule()) + .registerModule(SimpleModule().addDeserializer(String::class.java, TrimmingStringDeserializer())) + fun createAuthUser( id: Long = 0L, diff --git a/backend/src/test/kotlin/com/tul/backend/auth/controller/AuthUserControllerTests.kt b/backend/src/test/kotlin/com/tul/backend/auth/controller/AuthUserControllerTests.kt index 97fed35..3f501f1 100644 --- a/backend/src/test/kotlin/com/tul/backend/auth/controller/AuthUserControllerTests.kt +++ b/backend/src/test/kotlin/com/tul/backend/auth/controller/AuthUserControllerTests.kt @@ -1,11 +1,9 @@ package com.tul.backend.auth.controller import com.tul.backend.auth.base.valueobject.EmailAddress -import com.tul.backend.auth.dto.AuthUserDTO import com.tul.backend.auth.dto.LoginDTO import com.tul.backend.auth.dto.RegisterDTO import com.tul.backend.auth.service.AuthUserService -import com.tul.backend.createAuthUser import io.kotest.core.spec.style.FeatureSpec import io.kotest.matchers.shouldBe import io.mockk.every @@ -61,14 +59,13 @@ class AuthUserControllerTests : FeatureSpec({ "password", "password" ) - val authUserDTO = AuthUserDTO.from(createAuthUser()) - every { spec.authUserService.register(registerDTO) } returns authUserDTO + every { spec.authUserService.register(registerDTO) } returns true val result = spec.authUserController.register(registerDTO) result.statusCode shouldBe HttpStatus.OK - result.body shouldBe authUserDTO + result.body shouldBe true } scenario("register with invalid credentials") { @@ -80,12 +77,12 @@ class AuthUserControllerTests : FeatureSpec({ "password" ) - every { spec.authUserService.register(registerDTO) } returns null + every { spec.authUserService.register(registerDTO) } returns false val result = spec.authUserController.register(registerDTO) result.statusCode shouldBe HttpStatus.BAD_REQUEST - result.body shouldBe null + result.body shouldBe false } } }) diff --git a/backend/src/test/kotlin/com/tul/backend/auth/service/AuthUserServiceTests.kt b/backend/src/test/kotlin/com/tul/backend/auth/service/AuthUserServiceTests.kt index cd1dc2e..cb33367 100644 --- a/backend/src/test/kotlin/com/tul/backend/auth/service/AuthUserServiceTests.kt +++ b/backend/src/test/kotlin/com/tul/backend/auth/service/AuthUserServiceTests.kt @@ -5,7 +5,6 @@ import com.tul.backend.auth.dto.LoginDTO import com.tul.backend.auth.dto.RegisterDTO import com.tul.backend.auth.entity.AuthUser import com.tul.backend.auth.repository.AuthUserRepository -import com.tul.backend.createAuthUser import io.kotest.core.spec.style.FeatureSpec import io.kotest.matchers.shouldBe import io.mockk.every @@ -90,7 +89,7 @@ class AuthUserServiceTests : FeatureSpec({ "password", "password" ) - val authUser = createAuthUser() + val authUser = AuthUser.from(registerDTO) val authUserSlot = slot() @@ -98,12 +97,14 @@ class AuthUserServiceTests : FeatureSpec({ every { spec.authenticationHandler.hashRegistrationPassword(registerDTO) } returns authUser every { spec.authUserRepository.save(capture(authUserSlot)) } returnsArgument 0 - val result = spec.authUserService.register(registerDTO)!! + val result = spec.authUserService.register(registerDTO) val captured = authUserSlot.captured - result.id shouldBe captured.id - result.username shouldBe captured.username - result.email shouldBe captured.email + result shouldBe true + captured.username shouldBe registerDTO.username + captured.email shouldBe registerDTO.email + captured.password shouldBe authUser.password + registerDTO.passwordConfirmation shouldBe "password" } scenario("register with invalid email") { @@ -117,7 +118,7 @@ class AuthUserServiceTests : FeatureSpec({ val result = spec.authUserService.register(registerDTO) - result shouldBe null + result shouldBe false verify(exactly = 0) { spec.authenticationHandler.hashRegistrationPassword(any()) } verify(exactly = 0) { spec.authUserRepository.existsByEmail(any()) } verify(exactly = 0) { spec.authUserRepository.save(any()) } @@ -134,7 +135,7 @@ class AuthUserServiceTests : FeatureSpec({ val result = spec.authUserService.register(registerDTO) - result shouldBe null + result shouldBe false verify(exactly = 0) { spec.authenticationHandler.hashRegistrationPassword(any()) } verify(exactly = 0) { spec.authUserRepository.existsByEmail(any()) } verify(exactly = 0) { spec.authUserRepository.save(any()) } @@ -151,7 +152,7 @@ class AuthUserServiceTests : FeatureSpec({ val result = spec.authUserService.register(registerDTO) - result shouldBe null + result shouldBe false verify(exactly = 0) { spec.authenticationHandler.hashRegistrationPassword(any()) } verify(exactly = 0) { spec.authUserRepository.existsByEmail(any()) } verify(exactly = 0) { spec.authUserRepository.save(any()) } @@ -171,10 +172,27 @@ class AuthUserServiceTests : FeatureSpec({ val result = spec.authUserService.register(registerDTO) - result shouldBe null + result shouldBe false verify(exactly = 0) { spec.authenticationHandler.hashRegistrationPassword(any()) } verify(exactly = 0) { spec.authUserRepository.save(any()) } } + + scenario("register with not same passwords") { + val spec = getSpec() + val registerDTO = RegisterDTO( + "username", + EmailAddress("test@test.cz"), + "password", + "password123" + ) + + val result = spec.authUserService.register(registerDTO) + + result shouldBe false + verify(exactly = 0) { spec.authenticationHandler.hashRegistrationPassword(any()) } + verify(exactly = 0) { spec.authUserRepository.existsByEmail(any()) } + verify(exactly = 0) { spec.authUserRepository.save(any()) } + } } }) diff --git a/backend/src/test/kotlin/com/tul/backend/shared/jackson/TrimmingStringDeserializerTests.kt b/backend/src/test/kotlin/com/tul/backend/shared/jackson/TrimmingStringDeserializerTests.kt new file mode 100644 index 0000000..738f1ca --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/shared/jackson/TrimmingStringDeserializerTests.kt @@ -0,0 +1,31 @@ +package com.tul.backend.shared.jackson + +import com.tul.backend.objectMapper +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe + +class TrimmingStringDeserializerTest : FeatureSpec({ + + feature("deserialize") { + scenario("deserialize should trim the string") { + val jsonString = """{"value":" test "}""" + + val result = objectMapper.readValue(jsonString, TestDTO::class.java) + + result.value shouldBe "test" + } + } + + feature("serialize") { + scenario("serialize should not trim the string") { + val expectedResult = """{"value":" test "}""" + val test = TestDTO(" test ") + + val result = objectMapper.writeValueAsString(test) + + result shouldBe expectedResult + } + } +}) + +private data class TestDTO(val value: String) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/tul/backend/weather/controller/WeatherControllerTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/controller/WeatherControllerTests.kt new file mode 100644 index 0000000..1e9e24c --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/controller/WeatherControllerTests.kt @@ -0,0 +1,95 @@ +package com.tul.backend.weather.controller + +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.dto.ForecastWeatherDTO +import com.tul.backend.weather.service.WeatherService +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.springframework.http.HttpStatus +import java.time.LocalDate +import java.time.LocalDateTime + +class WeatherControllerTests : FeatureSpec({ + + feature("getCurrentWeather") { + scenario("should return current weather") { + val spec = getSpec() + val currentWeather = CurrentWeatherDTO( + time = LocalDateTime.now(), + temperature = 10.0, + cloudCover = 90, + windSpeed = 10.0, + isDay = true, + ) + + every { spec.weatherService.getCurrentWeather("test") } returns currentWeather + + val response = spec.weatherController.getCurrentWeather("test") + + response.body shouldBe currentWeather + response.statusCode shouldBe HttpStatus.OK + val body = response.body!! + body.time shouldBe currentWeather.time + body.temperature shouldBe currentWeather.temperature + body.cloudCover shouldBe currentWeather.cloudCover + body.windSpeed shouldBe currentWeather.windSpeed + body.isDay shouldBe currentWeather.isDay + } + + scenario("should return null with status not fount") { + val spec = getSpec() + + every { spec.weatherService.getCurrentWeather("test") } returns null + + val response = spec.weatherController.getCurrentWeather("test") + + response.body shouldBe null + response.statusCode shouldBe HttpStatus.NOT_FOUND + } + } + + feature("getForecastWeather") { + scenario("should return forecast weather") { + val spec = getSpec() + val forecastWeather = ForecastWeatherDTO( + time = listOf(LocalDate.now()), + maxTemperature = listOf(10.0), + minTemperature = listOf(5.0), + maxWindSpeed = listOf(10.0) + ) + + every { spec.weatherService.getForecastWeather("test") } returns forecastWeather + + val response = spec.weatherController.getForecastWeather("test") + + response.body shouldBe forecastWeather + response.statusCode shouldBe HttpStatus.OK + val body = response.body!! + body.time shouldBe forecastWeather.time + body.maxTemperature shouldBe forecastWeather.maxTemperature + body.minTemperature shouldBe forecastWeather.minTemperature + body.maxWindSpeed shouldBe forecastWeather.maxWindSpeed + } + + scenario("should return null with status not fount") { + val spec = getSpec() + + every { spec.weatherService.getForecastWeather("test") } returns null + + val response = spec.weatherController.getForecastWeather("test") + + response.body shouldBe null + response.statusCode shouldBe HttpStatus.NOT_FOUND + } + } +}) + +private class WeatherControllerSpecWrapper( + val weatherService: WeatherService +) { + val weatherController = WeatherController(weatherService) +} + +private fun getSpec() = WeatherControllerSpecWrapper(mockk()) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/tul/backend/weather/service/WeatherHandlerTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/service/WeatherHandlerTests.kt new file mode 100644 index 0000000..0da3c79 --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/service/WeatherHandlerTests.kt @@ -0,0 +1,54 @@ +package com.tul.backend.weather.service + +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.service.extractor.CurrentWeatherExtractorImpl +import com.tul.backend.weather.service.extractor.ForecastWeatherExtractorImpl +import com.tul.backend.weather.service.extractor.WeatherExtractor +import com.tul.backend.weather.service.parser.WebClientParser +import com.tul.backend.weather.valueobject.WeatherStatus +import io.kotest.core.annotation.Ignored +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import java.time.LocalDateTime + +@Ignored +class WeatherHandlerTests : FeatureSpec({ + + feature("getCurrentWeather") { + scenario("should return current weather") { + val spec = getSpec() + val location = "location" + val currentWeatherDTO = CurrentWeatherDTO( + time = LocalDateTime.now(), + temperature = 20.0, + cloudCover = 50, + windSpeed = 10.0, + isDay = true + ) + + every { spec.weatherExtractor[0].weatherStatus } returns WeatherStatus.CURRENT + every { spec.weatherExtractor[1].weatherStatus } returns WeatherStatus.FORECAST + every { spec.weatherExtractor[0].getWeatherJson(location) } returns "weatherJson" + every { spec.webClientParser.parseCurrentWeatherJson("weatherJson") } returns currentWeatherDTO + + val result = spec.weatherHandler.getCurrentWeather(location) + + result shouldBe currentWeatherDTO + } + + } +}) + +private class WeatherHandlerSpecWrapper( + var weatherExtractor: List, + var webClientParser: WebClientParser +) { + val weatherHandler = WeatherHandler(weatherExtractor, webClientParser) +} + +private fun getSpec() = WeatherHandlerSpecWrapper( + listOf(mockk(), mockk()), + mockk() +) diff --git a/backend/src/test/kotlin/com/tul/backend/weather/service/WeatherServiceTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/service/WeatherServiceTests.kt new file mode 100644 index 0000000..53df2c7 --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/service/WeatherServiceTests.kt @@ -0,0 +1,83 @@ +package com.tul.backend.weather.service + +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.dto.ForecastWeatherDTO +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import java.time.LocalDate +import java.time.LocalDateTime + +class WeatherServiceTests : FeatureSpec({ + + feature("getCurrentWeather") { + scenario("should return current weather") { + val spec = getSpec() + val location = "location" + val currentWeatherDTO = CurrentWeatherDTO( + time = LocalDateTime.now(), + temperature = 20.0, + cloudCover = 50, + windSpeed = 10.0, + isDay = true + ) + + every { spec.weatherHandler.getCurrentWeather(location) } returns currentWeatherDTO + + val result = spec.weatherService.getCurrentWeather(location) + + result shouldBe currentWeatherDTO + } + + scenario("should return nul, when current weather is not found") { + val spec = getSpec() + val location = "location" + + every { spec.weatherHandler.getCurrentWeather(location) } returns null + + val result = spec.weatherService.getCurrentWeather(location) + + result shouldBe null + } + } + + feature("getForecastWeather") { + + scenario("should return forecast weather") { + val spec = getSpec() + val location = "location" + val forecastWeatherDTO = ForecastWeatherDTO( + time = listOf(LocalDate.now()), + maxTemperature = listOf(20.0), + minTemperature = listOf(10.0), + maxWindSpeed = listOf(10.0), + ) + + every { spec.weatherHandler.getForecastWeather(location) } returns forecastWeatherDTO + + val result = spec.weatherService.getForecastWeather(location) + + result shouldBe forecastWeatherDTO + } + + scenario("should return nul, when forecast weather is not found") { + val spec = getSpec() + val location = "location" + + every { spec.weatherHandler.getForecastWeather(location) } returns null + + val result = spec.weatherService.getForecastWeather(location) + + result shouldBe null + } + } +}) + +private class WeatherServiceSpecWrapper( + var weatherHandler: WeatherHandler +) { + val weatherService = WeatherService(weatherHandler) +} + +private fun getSpec() = WeatherServiceSpecWrapper(mockk()) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/tul/backend/weather/service/client/GeoDecoderClientServiceTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/service/client/GeoDecoderClientServiceTests.kt new file mode 100644 index 0000000..39e5cfc --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/service/client/GeoDecoderClientServiceTests.kt @@ -0,0 +1,31 @@ +package com.tul.backend.weather.service.client + +import com.tul.backend.IntegrationTestApplication +import io.kotest.core.annotation.Ignored +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.web.reactive.function.client.WebClient + +@Ignored +@SpringBootTest(classes = [IntegrationTestApplication::class]) +class GeoDecoderClientServiceTests : FunSpec({ + + test("getLocation should return locationJson") { + val webClient = mockk() + val webClientBuilder = mockk() + val geoDecoderClientService = GeoDecoderClientService(webClientBuilder) + + every { webClientBuilder.clientConnector(any()) } returns webClientBuilder + every { webClientBuilder.codecs(any()) } returns webClientBuilder + every { webClientBuilder.baseUrl("https://geocoding-api.open-meteo.com/v1/search") } returns webClientBuilder + every { webClientBuilder.build() } returns webClient + + + val locationJson = geoDecoderClientService.getLocation("location") + + locationJson shouldBe "locationJson" + } +}) diff --git a/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/CurrentWeatherExtractorImplTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/CurrentWeatherExtractorImplTests.kt new file mode 100644 index 0000000..ccab9ce --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/CurrentWeatherExtractorImplTests.kt @@ -0,0 +1,90 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.service.client.OpenMeteoClientService +import com.tul.backend.weather.valueobject.WeatherStatus.CURRENT +import com.tul.backend.weather.valueobject.WebClientOutcome +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk + +class CurrentWeatherExtractorImplTests : FeatureSpec({ + + feature("getWeatherJson") { + scenario("should return current weather json") { + val spec = getSpec() + val location = "Liberec" + val locationDTO = LocationDTO( + latitude = 50.7702648, + longitude = 15.0583947 + ) + val webClientOutcome = WebClientOutcome.Success("locationJson") + + coEvery { spec.locationExtractor.getLocation(location) } returns locationDTO + coEvery { spec.openMeteoClientService.getCurrentWeather(locationDTO) } returns webClientOutcome + + val result = spec.currentWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe webClientOutcome.response + spec.currentWeatherExtractorImpl.weatherStatus shouldBe CURRENT + } + + scenario("location not found") { + val spec = getSpec() + val location = "Liberec" + + coEvery { spec.locationExtractor.getLocation(location) } returns null + + val result = spec.currentWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe null + coVerify(exactly = 0) { spec.openMeteoClientService.getCurrentWeather(any()) } + } + + scenario("web client server error") { + val spec = getSpec() + val location = "Liberec" + val locationDTO = LocationDTO( + latitude = 50.7702648, + longitude = 15.0583947 + ) + val webClientOutcome = WebClientOutcome.Failure.ServerError + + coEvery { spec.locationExtractor.getLocation(location) } returns locationDTO + coEvery { spec.openMeteoClientService.getCurrentWeather(locationDTO) } returns webClientOutcome + + val result = spec.currentWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe null + } + + scenario("web client unspecified error") { + val spec = getSpec() + val location = "Liberec" + val locationDTO = LocationDTO( + latitude = 50.7702648, + longitude = 15.0583947 + ) + val webClientOutcome = WebClientOutcome.Failure.UnspecifiedError(Exception("error")) + + coEvery { spec.locationExtractor.getLocation(location) } returns locationDTO + coEvery { spec.openMeteoClientService.getCurrentWeather(locationDTO) } returns webClientOutcome + + val result = spec.currentWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe null + } + } +}) + +private class CurrentWeatherExtractorImplSpecWrapper( + val locationExtractor: LocationExtractor, + val openMeteoClientService: OpenMeteoClientService +) { + val currentWeatherExtractorImpl = CurrentWeatherExtractorImpl(locationExtractor, openMeteoClientService) +} + +private fun getSpec() = CurrentWeatherExtractorImplSpecWrapper(mockk(), mockk()) + diff --git a/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/ForecastWeatherExtractorImplTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/ForecastWeatherExtractorImplTests.kt new file mode 100644 index 0000000..f563f57 --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/ForecastWeatherExtractorImplTests.kt @@ -0,0 +1,89 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.service.client.OpenMeteoClientService +import com.tul.backend.weather.valueobject.WeatherStatus.FORECAST +import com.tul.backend.weather.valueobject.WebClientOutcome +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk + +class ForecastWeatherExtractorImplTests : FeatureSpec({ + + feature("getWeatherJson") { + scenario("should return forecast weather json") { + val spec = getSpec() + val location = "Liberec" + val locationDTO = LocationDTO( + latitude = 50.7702648, + longitude = 15.0583947 + ) + val webClientOutcome = WebClientOutcome.Success("locationJson") + + coEvery { spec.locationExtractor.getLocation(location) } returns locationDTO + coEvery { spec.openMeteoClientService.getForecastWeather(locationDTO) } returns webClientOutcome + + val result = spec.forecastWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe webClientOutcome.response + spec.forecastWeatherExtractorImpl.weatherStatus shouldBe FORECAST + } + + scenario("location not found") { + val spec = getSpec() + val location = "Liberec" + + coEvery { spec.locationExtractor.getLocation(location) } returns null + + val result = spec.forecastWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe null + coVerify(exactly = 0) { spec.openMeteoClientService.getForecastWeather(any()) } + } + + scenario("web client server error") { + val spec = getSpec() + val location = "Liberec" + val locationDTO = LocationDTO( + latitude = 50.7702648, + longitude = 15.0583947 + ) + val webClientOutcome = WebClientOutcome.Failure.ServerError + + coEvery { spec.locationExtractor.getLocation(location) } returns locationDTO + coEvery { spec.openMeteoClientService.getForecastWeather(locationDTO) } returns webClientOutcome + + val result = spec.forecastWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe null + } + + scenario("web client unspecified error") { + val spec = getSpec() + val location = "Liberec" + val locationDTO = LocationDTO( + latitude = 50.7702648, + longitude = 15.0583947 + ) + val webClientOutcome = WebClientOutcome.Failure.UnspecifiedError(Exception("error")) + + coEvery { spec.locationExtractor.getLocation(location) } returns locationDTO + coEvery { spec.openMeteoClientService.getForecastWeather(locationDTO) } returns webClientOutcome + + val result = spec.forecastWeatherExtractorImpl.getWeatherJson(location) + + result shouldBe null + } + } +}) + +private class ForecastWeatherExtractorImplSpecWrapper( + val locationExtractor: LocationExtractor, + val openMeteoClientService: OpenMeteoClientService +) { + val forecastWeatherExtractorImpl = ForecastWeatherExtractorImpl(locationExtractor, openMeteoClientService) +} + +private fun getSpec() = ForecastWeatherExtractorImplSpecWrapper(mockk(), mockk()) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/LocationExtractorImplTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/LocationExtractorImplTests.kt new file mode 100644 index 0000000..118fc1f --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/service/extractor/LocationExtractorImplTests.kt @@ -0,0 +1,70 @@ +package com.tul.backend.weather.service.extractor + +import com.tul.backend.weather.dto.LocationDTO +import com.tul.backend.weather.service.client.GeoDecoderClientService +import com.tul.backend.weather.service.parser.WebClientParser +import com.tul.backend.weather.valueobject.WebClientOutcome +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class LocationExtractorImplTests : FeatureSpec({ + + feature("getWeatherJson") { + scenario("should return forecast weather json") { + val spec = getSpec() + val location = "Liberec" + val locationDTO = LocationDTO( + latitude = 50.7702648, + longitude = 15.0583947 + ) + val webClientOutcome = WebClientOutcome.Success("locationJson") + + coEvery { spec.geoDecoderClientService.getLocation(location) } returns webClientOutcome + every { spec.webClientParser.parseLocationJson(webClientOutcome.response) } returns locationDTO + + val result = spec.forecastWeatherExtractorImpl.getLocation(location)!! + + result.latitude shouldBe locationDTO.latitude + result.longitude shouldBe locationDTO.longitude + } + + scenario("web client server error") { + val spec = getSpec() + val location = "Liberec" + val webClientOutcome = WebClientOutcome.Failure.ServerError + + coEvery { spec.geoDecoderClientService.getLocation(location) } returns webClientOutcome + + val result = spec.forecastWeatherExtractorImpl.getLocation(location) + + result shouldBe null + verify(exactly = 0) { spec.webClientParser.parseLocationJson(any()) } + } + + scenario("web client unspecified error") { + val spec = getSpec() + val location = "Liberec" + val webClientOutcome = WebClientOutcome.Failure.UnspecifiedError(Exception("test")) + + coEvery { spec.geoDecoderClientService.getLocation(location) } returns webClientOutcome + + val result = spec.forecastWeatherExtractorImpl.getLocation(location) + + result shouldBe null + verify(exactly = 0) { spec.webClientParser.parseLocationJson(any()) } + } + } +}) + +private class LocationExtractorImplSpecWrapper( + val geoDecoderClientService: GeoDecoderClientService, + val webClientParser: WebClientParser +) { + val forecastWeatherExtractorImpl = LocationExtractorImpl(geoDecoderClientService, webClientParser) +} + +private fun getSpec() = LocationExtractorImplSpecWrapper(mockk(), mockk()) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/tul/backend/weather/service/parser/WebClientParserTests.kt b/backend/src/test/kotlin/com/tul/backend/weather/service/parser/WebClientParserTests.kt new file mode 100644 index 0000000..dc9b711 --- /dev/null +++ b/backend/src/test/kotlin/com/tul/backend/weather/service/parser/WebClientParserTests.kt @@ -0,0 +1,128 @@ +package com.tul.backend.weather.service.parser + +import com.tul.backend.objectMapper +import com.tul.backend.weather.dto.CurrentWeatherDTO +import com.tul.backend.weather.dto.ForecastWeatherDTO +import com.tul.backend.weather.dto.LocationDTO +import io.kotest.core.spec.style.FeatureSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDate +import java.time.LocalDateTime + +class WebClientParserTests : FeatureSpec({ + + feature("parseLocationJson") { + scenario("should return LocationDTO") { + val spec = getSpec() + val json = """ + { + "results": [ + { + "latitude": 51.506321, + "longitude": -0.12714 + } + ] + } + """.trimIndent() + val expectedResult = LocationDTO(51.506321, -0.12714) + + val result = spec.webClientParser.parseLocationJson(json)!! + + result.latitude shouldBe expectedResult.latitude + result.longitude shouldBe expectedResult.longitude + } + + scenario("should return null") { + val spec = getSpec() + val json = "invalid json" + + val result = spec.webClientParser.parseLocationJson(json) + + result shouldBe null + } + } + + feature("parseCurrentWeatherJson") { + scenario("should return CurrentWeatherDTO") { + val spec = getSpec() + val json = """ + { + "current": { + "time": "2021-08-01T00:00:00.000Z", + "temperature_2m": 15.0, + "cloud_cover": 1, + "is_day": 1, + "wind_speed_10": 20.0 + } + } + """.trimIndent() + val expectedResult = CurrentWeatherDTO( + time = LocalDateTime.parse("2021-08-01T00:00:00"), + temperature = 15.0, + cloudCover = 1, + windSpeed = 20.0, + isDay = true + ) + + val result = spec.webClientParser.parseCurrentWeatherJson(json)!! + + result.time shouldBe expectedResult.time + result.temperature shouldBe expectedResult.temperature + result.cloudCover shouldBe expectedResult.cloudCover + result.windSpeed shouldBe expectedResult.windSpeed + result.isDay shouldBe expectedResult.isDay + } + + scenario("should return null") { + val spec = getSpec() + val json = "invalid json" + + val result = spec.webClientParser.parseCurrentWeatherJson(json) + + result shouldBe null + } + } + + feature("parseForecastWeatherJson") { + scenario("should return ForecastWeatherDTO") { + val spec = getSpec() + val json = """ + { + "daily": { + "time": ["2021-08-01T00:00:00.000Z"], + "temperature_2m_max": [15.0], + "temperature_2m_min": [10.0],"wind_speed_10m_max": [20.0] + } + } + """.trimMargin() + val expectedResult = ForecastWeatherDTO( + time = listOf(LocalDate.parse("2021-08-01")), + maxTemperature = listOf(15.0), + minTemperature = listOf(10.0), + maxWindSpeed = listOf(20.0) + ) + + val result = spec.webClientParser.parseForecastWeatherJson(json)!! + + result.time shouldBe expectedResult.time + result.maxTemperature shouldBe expectedResult.maxTemperature + result.minTemperature shouldBe expectedResult.minTemperature + result.maxWindSpeed shouldBe expectedResult.maxWindSpeed + } + + scenario("should return null") { + val spec = getSpec() + val json = "invalid json" + + val result = spec.webClientParser.parseForecastWeatherJson(json) + + result shouldBe null + } + } +}) + +private class WebClientParserSpecWrapper { + val webClientParser = WebClientParser(objectMapper) +} + +private fun getSpec() = WebClientParserSpecWrapper() \ No newline at end of file diff --git a/frontend/angular.json b/frontend/angular.json index ca8fda1..79bc874 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -49,7 +49,13 @@ "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] } }, "defaultConfiguration": "production" @@ -61,7 +67,7 @@ "buildTarget": "frontend:build:production" }, "development": { - "buildTarget": "frontend:build:development" + "buildTarget": "frontend:build:development", } }, "defaultConfiguration": "development" diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 745bdd6..fed6a65 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -8,7 +8,7 @@ const jestConfig: Config = { reporters: [ 'default', ], - collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'], + collectCoverageFrom: ['src/app/*.ts', '!**/node_modules/**'], coverageDirectory: 'coverage', coverageReporters: ['cobertura', 'text', 'text-summary', 'html'], cacheDirectory: '.jestcache' diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 53bb0f5..dd3fe9c 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -2,9 +2,10 @@ import {RouterModule, Routes} from "@angular/router"; import {NgModule} from "@angular/core"; import {LoginComponent} from "./auth/login/login.component"; import {RegisterComponent} from "./auth/register/register.component"; +import {WeatherDetailComponent} from "./weather/weather-detail/weather-detail.component"; const routes: Routes = [ - {path: '', component: LoginComponent}, + {path: '', component: WeatherDetailComponent}, {path: 'login', component: LoginComponent}, {path: 'registration', component: RegisterComponent} ]; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index bed38d4..4387cbb 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -2,11 +2,15 @@ import {NgModule} from "@angular/core"; import {AppComponent} from "./app.component"; import {HTTP_INTERCEPTORS, HttpClientModule} from "@angular/common/http"; import {AppRoutingModule} from "./app-routing.module"; -import {FormsModule} from "@angular/forms"; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {BrowserModule} from "@angular/platform-browser"; import {RouterModule} from "@angular/router"; import {HttpHeaderInterceptor} from "./shared/http/interceptor/http-header.interceptor"; import {BrowserAnimationsModule, provideAnimations} from "@angular/platform-browser/animations"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatInputModule} from "@angular/material/input"; +import {MatButtonModule} from "@angular/material/button"; +import {MatIconModule} from "@angular/material/icon"; @NgModule({ declarations: [ @@ -18,7 +22,13 @@ import {BrowserAnimationsModule, provideAnimations} from "@angular/platform-brow HttpClientModule, FormsModule, RouterModule, - BrowserAnimationsModule + BrowserAnimationsModule, + MatFormFieldModule, + MatFormFieldModule, + MatInputModule, + FormsModule, + MatButtonModule, + MatIconModule ], providers: [ { diff --git a/frontend/src/app/auth/login/login.component.html b/frontend/src/app/auth/login/login.component.html index 04bf5aa..46cad2b 100644 --- a/frontend/src/app/auth/login/login.component.html +++ b/frontend/src/app/auth/login/login.component.html @@ -1,13 +1,13 @@
-