diff --git a/README.md b/README.md index 676a872e4698e726ceae6fdb6b8224527a2e9405..c4e0639291cafbeb71bd66b9f6ea103df2e0b4e5 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,17 @@ This will delete existing images. ### To stop the application (all microservices) $ podman-compose down +## Observations + +### Prometheus + +The prometheus collects the metrics from all 4 microservices. The UI runs at the http://localhost:9090. + +### Grafana + +Grafana runs at http://localhost:3000. To Log in, use the username `admin` and the password `admin`. +There is already one minimalistic dashboard imported. However, feel free to experiment and visualize another attributes. + ## Usage ### Used ports + Authorization - port 8083 diff --git a/authorization/pom.xml b/authorization/pom.xml index 3bd224e0f1fcb72f33309323e745bdff5fdf4886..27ba936bdd041e57c72ca922662106cdfab1334d 100644 --- a/authorization/pom.xml +++ b/authorization/pom.xml @@ -21,6 +21,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> diff --git a/authorization/src/main/resources/application.yml b/authorization/src/main/resources/application.yml index 86820bd5b40c31e85ed3ec1d44cc4cba01a5cfd4..315986cde4c4506667763774d0d1555d23be738a 100644 --- a/authorization/src/main/resources/application.yml +++ b/authorization/src/main/resources/application.yml @@ -7,4 +7,24 @@ spring: jpa: database-platform: org.hibernate.dialect.H2Dialect server: - port: 8083 \ No newline at end of file + port: 8083 +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' diff --git a/compose.yaml b/compose.yaml index ac0b2752b1b9eaf650eb4758ae364732253dd54b..5cf3f15a109c22369e52128ea03ef1ae1895cb8a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,9 @@ version: "3" + +networks: + grafana-prometheus: + driver: bridge + services: core: build: "./core" @@ -18,4 +23,24 @@ services: weather: build: "./weather" ports: - - "8088:8088" \ No newline at end of file + - "8088:8088" + + prometheus: + image: prom/prometheus:v2.43.0 + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + command: --config.file=/etc/prometheus/prometheus.yml + networks: + - grafana-prometheus + ports: + - '9090:9090' + + grafana: + image: grafana/grafana:9.1.7 + volumes: + - ./datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml + - ./dashboards:/etc/grafana/provisioning/dashboards + networks: + - grafana-prometheus + ports: + - '3000:3000' diff --git a/core/pom.xml b/core/pom.xml index 362c646dcfbe14deef9d7656d0b78a2f8d55b0df..d1513243a72cfa6d0ef1c552b121056003ba2d4b 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -21,6 +21,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> diff --git a/core/src/main/resources/application.yml b/core/src/main/resources/application.yml index 1af7b28b0c5756033a40a681f58474c86d43602a..9824e82b44b8bea2e2041ad0c98de43c5b71b6fd 100644 --- a/core/src/main/resources/application.yml +++ b/core/src/main/resources/application.yml @@ -12,3 +12,23 @@ spring: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: update +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' diff --git a/dashboards/dashboard.yaml b/dashboards/dashboard.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b5df708530ea53752bf1bf1b926acc851d3d83b9 --- /dev/null +++ b/dashboards/dashboard.yaml @@ -0,0 +1,7 @@ +apiVersion: 1 + +providers: + - name: 'Default' + folder: 'Services' + options: + path: /etc/grafana/provisioning/dashboards diff --git a/dashboards/grafana.json b/dashboards/grafana.json new file mode 100644 index 0000000000000000000000000000000000000000..c035a36562fb1b1c2185b4bab5f66d7f6a53c35b --- /dev/null +++ b/dashboards/grafana.json @@ -0,0 +1,109 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": {}, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 8, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.1.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "p5SIZnEVk" + }, + "editorMode": "code", + "expr": "http_server_requests_seconds_count", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "HTTP Server Requests counter", + "type": "gauge" + } + ], + "refresh": "5s", + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Airport-Manager", + "uid": "YDzAhnEVz", + "version": 3, + "weekStart": "" +} diff --git a/datasource.yml b/datasource.yml new file mode 100644 index 0000000000000000000000000000000000000000..d96153aa026474ed8aa7882e1586d0334623d4e1 --- /dev/null +++ b/datasource.yml @@ -0,0 +1,7 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000000000000000000000000000000000000..fadedee9716b5b5de22dbd23a934a34becb77993 --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 1s + external_labels: + monitor: 'my-monitor' + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'microservices' + metrics_path: /actuator/prometheus + static_configs: + - targets: ['localhost:8080'] + - targets: ['localhost:8083'] + - targets: ['localhost:8085'] + - targets: ['localhost:8088'] diff --git a/report/pom.xml b/report/pom.xml index 80b8165a84a93096adacc4489b59d6dac5cbb72c..dc247ce2039a49158919f914a51d55810fa19efc 100644 --- a/report/pom.xml +++ b/report/pom.xml @@ -21,6 +21,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> diff --git a/report/src/main/resources/application.yaml b/report/src/main/resources/application.yaml index 0abcd741f27e93ab0d76b682b58e413b874d2c92..26f8a4475b57efeec939c295d78edc73022d77a5 100644 --- a/report/src/main/resources/application.yaml +++ b/report/src/main/resources/application.yaml @@ -10,3 +10,23 @@ spring: # for now to not collide with another microservices server: port: 8085 +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' diff --git a/weather/openapi.yaml b/weather/openapi.yaml index 458c08ad7f59e878060030fc07240ce579b479f9..d36f8a8feaae9ebb626d49993ab295a3e0cb680e 100644 --- a/weather/openapi.yaml +++ b/weather/openapi.yaml @@ -72,7 +72,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/FlightCreationAdvice' + $ref: '#/components/schemas/FlightCreationAdviceDto' "400": description: Input data not correct content: @@ -107,7 +107,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/WeatherForecast' + $ref: '#/components/schemas/WeatherForecastDto' "400": description: Input data not correct content: @@ -116,34 +116,24 @@ paths: $ref: '../core/openapi.yaml#/components/schemas/ErrorMessage' components: schemas: - WeatherReason: - title: Weather reason + FlightCreationAdviceDto: + title: FlightCreationAdviceDto description: | - Enumeration of weather reasons, e.g. whether it is ok - to fly or the reason why not - type: string - example: ok-everything - enum: - - ok-everything - - nok-wind - - nok-temperature - - nok-rain - FlightCreationAdvice: - title: Flight creation advice - description: | - Response of '/api/isSafeToCreateFlight' call. + Flight Creation Advice DTO type: object properties: result: type: boolean description: Result whether it's safe to create such a flight reason: - $ref: '#/components/schemas/WeatherReason' - WeatherForecast: - title: Weather forecast + $ref: '#/components/schemas/WeatherReasonDto' + required: + - result + - reason + WeatherForecastDto: + title: WeatherForecastDto description: | - Information about weather forecast including basic attributes, - e.g. wind speed + Weather Forecast DTO type: object properties: temperature: @@ -160,7 +150,26 @@ components: type: number format: double description: Sum of rain for preceding hour in millimeters + example: 0.3 visibility: type: number format: double description: Visibility in kilometers + example: 12 + required: + - temperature + - windSpeed + - rain + - visibility + WeatherReasonDto: + title: WeatherReasonDto + description: | + Weather Reason DTO + type: string + example: ok-everything + enum: + - ok-everything + - nok-temperature + - nok-wind + - nok-rain + - nok-visibility diff --git a/weather/pom.xml b/weather/pom.xml index 5ac0386510bd1147f866e0edc455cdf3da5190aa..ed218727319ef5e614bebbf8a1c49abba97ee07a 100644 --- a/weather/pom.xml +++ b/weather/pom.xml @@ -14,6 +14,18 @@ <description>Weather forecast service for Airport Manager</description> <dependencies> + <dependency> + <groupId>cz.muni.fi.pa165</groupId> + <artifactId>core-client</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>5.3.1</version> + <scope>test</scope> + </dependency> <dependency> <groupId>com.h2database</groupId> @@ -21,6 +33,18 @@ <scope>runtime</scope> </dependency> + <!-- Actuator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> + + <!-- Prometheus --> + <dependency> + <groupId>io.micrometer</groupId> + <artifactId>micrometer-registry-prometheus</artifactId> + </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> @@ -67,11 +91,16 @@ <artifactId>spring-data-commons</artifactId> </dependency> + <!-- Bean validator --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + <!-- for testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> </dependency> </dependencies> diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/WeatherController.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/WeatherController.java deleted file mode 100644 index 867ead5308c16f2829a8922975b082bbe4f7b328..0000000000000000000000000000000000000000 --- a/weather/src/main/java/cz/muni/fi/pa165/weather/server/WeatherController.java +++ /dev/null @@ -1,32 +0,0 @@ -package cz.muni.fi.pa165.weather.server; - -import cz.muni.fi.pa165.weather.server.api.WeatherApiDelegate; -import cz.muni.fi.pa165.weather.server.model.FlightCreationAdvice; -import cz.muni.fi.pa165.weather.server.model.WeatherForecast; -import cz.muni.fi.pa165.weather.server.model.WeatherReason; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RestController; - -import java.time.OffsetDateTime; - -@RestController -public class WeatherController implements WeatherApiDelegate { - - @Override - public ResponseEntity<WeatherForecast> getWeatherForecast(Double latitude, Double longitude) { - var weatherForecast = new WeatherForecast() - .temperature(3.14) - .windSpeed(15.92) - .rain(0.8) - .visibility(6.7); - return ResponseEntity.ok(weatherForecast); - } - - @Override - public ResponseEntity<FlightCreationAdvice> isSafeToCreateFlight(Long departureAirportId, Long arrivalAirportId, OffsetDateTime departureTime, OffsetDateTime arrivalTime) { - var flightCreationAdvice = new FlightCreationAdvice() - .result(true) - .reason(WeatherReason.OK_EVERYTHING); - return ResponseEntity.ok(flightCreationAdvice); - } -} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/FlightCreationAdvice.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/FlightCreationAdvice.java new file mode 100644 index 0000000000000000000000000000000000000000..c9df61d2f13be6b0fc433b45e7efc1adfe840871 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/FlightCreationAdvice.java @@ -0,0 +1,15 @@ +package cz.muni.fi.pa165.weather.server.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@RequiredArgsConstructor +public class FlightCreationAdvice { + + private boolean result; + + private WeatherReason weatherReason; +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/HourlyWeatherForecast.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/HourlyWeatherForecast.java new file mode 100644 index 0000000000000000000000000000000000000000..912393b4521ca89a3d4134725db30ae07c62e750 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/HourlyWeatherForecast.java @@ -0,0 +1,43 @@ +package cz.muni.fi.pa165.weather.server.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; +import java.util.Map; + +/** + * Entity representation of the data from our Weather API we use, + * see: <a href="https://open-meteo.com/">open-meteo</a>. + */ +@Getter +@Setter +@RequiredArgsConstructor +@ToString +public class HourlyWeatherForecast { + + private double latitude; + + private double longitude; + + @JsonProperty("generationtime_ms") + private double generationTimeMs; + + @JsonProperty("utc_offset_seconds") + private long utcOffsetSeconds; + + private String timezone; + + @JsonProperty("timezone_abbreviation") + private String timezoneAbbreviation; + + private double elevation; + + @JsonProperty("hourly_units") + private Map<String, String> hourlyUnits; + + private Map<String, List<Object>> hourly; +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherForecast.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherForecast.java new file mode 100644 index 0000000000000000000000000000000000000000..f8f7da6e5045e76357182852db9fe76e5db3f8d8 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherForecast.java @@ -0,0 +1,48 @@ +package cz.muni.fi.pa165.weather.server.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.stereotype.Component; + +@Component +@Getter +@Setter +@RequiredArgsConstructor +public class WeatherForecast { + + private double temperature; + + private double windSpeed; + + private double rain; + + private double visibility; + + private static final double MIN_SAFE_TEMPERATURE = -40.0; + + private static final double MAX_SAFE_TEMPERATURE = 40.0; + + private static final double MAX_SAFE_WINDSPEED = 75; + + private static final double MAX_SAFE_RAIN = 3.0; + + private static final double MIN_SAFE_VISIBILITY = 1000.0; + + public boolean temperatureInSafeBounds() { + return MIN_SAFE_TEMPERATURE <= temperature && + temperature <= MAX_SAFE_TEMPERATURE; + } + + public boolean windSpeedInSafeBounds() { + return windSpeed <= MAX_SAFE_WINDSPEED; + } + + public boolean rainInSafeBounds() { + return rain <= MAX_SAFE_RAIN; + } + + public boolean visibilityInSafeBounds() { + return MIN_SAFE_VISIBILITY <= visibility; + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherReason.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherReason.java new file mode 100644 index 0000000000000000000000000000000000000000..26564c67ed23f90d7b17fef7b970e89ffd480c12 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/data/WeatherReason.java @@ -0,0 +1,10 @@ +package cz.muni.fi.pa165.weather.server.data; + +public enum WeatherReason { + + OK_EVERYTHING, + NOK_TEMPERATURE, + NOK_WIND, + NOK_RAIN, + NOK_VISIBILITY +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacade.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacade.java new file mode 100644 index 0000000000000000000000000000000000000000..5a176f54d58f85f1aaa2307966e9cd12a4dd5aa1 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacade.java @@ -0,0 +1,20 @@ +package cz.muni.fi.pa165.weather.server.facade; + +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; + +@Service +public interface WeatherFacade { + + WeatherForecastDto getWeatherForecast(Double latitude, Double longitude); + + FlightCreationAdviceDto isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeImpl.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4c0cc770af5d2bc44a21701c42008d06c5cd9ed6 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeImpl.java @@ -0,0 +1,55 @@ +package cz.muni.fi.pa165.weather.server.facade; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.mapper.FlightCreationAdviceMapper; +import cz.muni.fi.pa165.weather.server.mapper.WeatherForecastMapper; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import cz.muni.fi.pa165.weather.server.service.WeatherService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; + +@Service +public class WeatherFacadeImpl implements WeatherFacade { + + private final WeatherService weatherService; + private final WeatherForecastMapper weatherForecastMapper; + private final FlightCreationAdviceMapper flightCreationAdviceMapper; + + @Autowired + public WeatherFacadeImpl( + WeatherService weatherService, + WeatherForecastMapper weatherForecastMapper, + FlightCreationAdviceMapper flightCreationAdviceMapper + ) { + this.weatherService = weatherService; + this.weatherForecastMapper = weatherForecastMapper; + this.flightCreationAdviceMapper = flightCreationAdviceMapper; + } + + @Override + public WeatherForecastDto getWeatherForecast(Double latitude, Double longitude) { + WeatherForecast weatherForecast = weatherService.getWeatherForecast(latitude, longitude); + return weatherForecastMapper.toDto(weatherForecast); + } + + @Override + public FlightCreationAdviceDto isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ) { + FlightCreationAdvice flightCreationAdvice = weatherService + .isSafeToCreateFlight( + departureAirportId, + arrivalAirportId, + departureTime, + arrivalTime + ); + return flightCreationAdviceMapper.toDto(flightCreationAdvice); + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapper.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..ac8e77d1ed7c8b2c1fe5f3fb349f0ec15fb8fa5a --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapper.java @@ -0,0 +1,19 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = "spring") +public abstract class FlightCreationAdviceMapper { + + @Autowired + protected WeatherReasonMapper weatherReasonMapper; + + @Mapping(target = "reason", expression = "java(weatherReasonMapper.toDto(flightCreationAdvice.getWeatherReason()))") + public abstract FlightCreationAdviceDto toDto(FlightCreationAdvice flightCreationAdvice); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapper.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a58f996094daa4defa4ea0466284151532993ddd --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapper.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.mapstruct.Mapper; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = "spring") +public interface WeatherForecastMapper { + + WeatherForecastDto toDto(WeatherForecast weatherForecast); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapper.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..fa28b4d0da57321c544fec9b0de6c4adf515fbc1 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapper.java @@ -0,0 +1,13 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import org.mapstruct.Mapper; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = "spring") +public interface WeatherReasonMapper { + + WeatherReasonDto toDto(WeatherReason weatherReason); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/rest/WeatherController.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/rest/WeatherController.java new file mode 100644 index 0000000000000000000000000000000000000000..636a34224ab0e684842df9b763db7a356a1cbd9e --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/rest/WeatherController.java @@ -0,0 +1,47 @@ +package cz.muni.fi.pa165.weather.server.rest; + +import cz.muni.fi.pa165.weather.server.api.WeatherApiDelegate; +import cz.muni.fi.pa165.weather.server.facade.WeatherFacade; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.time.OffsetDateTime; + +@RestController +public class WeatherController implements WeatherApiDelegate { + + private final WeatherFacade weatherFacade; + + @Autowired + public WeatherController(WeatherFacade weatherFacade) { + this.weatherFacade = weatherFacade; + } + + @Override + public ResponseEntity<WeatherForecastDto> getWeatherForecast( + Double latitude, + Double longitude + ) { + return ResponseEntity.ok(weatherFacade.getWeatherForecast(latitude, longitude)); + } + + @Override + public ResponseEntity<FlightCreationAdviceDto> isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ) { + return ResponseEntity.ok( + weatherFacade.isSafeToCreateFlight( + departureAirportId, + arrivalAirportId, + departureTime, + arrivalTime + ) + ); + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/TimeUtils.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/TimeUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1f6642a1d515fee29337f2c0df072cdd65af5456 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/TimeUtils.java @@ -0,0 +1,29 @@ +package cz.muni.fi.pa165.weather.server.service; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; + +/** + * Utility class for work with {@link LocalDateTime}. + */ +class TimeUtils { + + private TimeUtils() { + // Intentionally made private to prevent instantiation. + } + + static LocalDateTime toLocalDateTime(OffsetDateTime offsetDateTime) { + return offsetDateTime + .atZoneSameInstant(ZoneId.of("UTC+02:00")) + .toLocalDateTime(); + } + + static int getActualHour() { + return getHourOf(LocalDateTime.now()); + } + + static int getHourOf(LocalDateTime localDateTime) { + return localDateTime.getHour(); + } +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherService.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherService.java new file mode 100644 index 0000000000000000000000000000000000000000..b93f76db9f23d19ed832ef68715ef090d14044ae --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherService.java @@ -0,0 +1,43 @@ +package cz.muni.fi.pa165.weather.server.service; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; + +@Service +public interface WeatherService { + + /** + * Get the weather info for the given {@code latitude} + * and {@code longitude} at this moment. + * + * @param latitude latitude + * @param longitude longitude + * @return weather information + */ + WeatherForecast getWeatherForecast( + double latitude, + double longitude + ); + + /** + * Find out whether it's safe to create the flight based + * on {@code departureAirportId}, {@code arrivalAirportId}, + * {@code departureTime} and {@code arrivalTime}. In case of + * that it isn't safe, returns also the reason why not. + * + * @param departureAirportId departure airport's id + * @param arrivalAirportId arrival airport's id + * @param departureTime flight departure time + * @param arrivalTime flight arrival time + * @return result as written above + */ + FlightCreationAdvice isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ); +} diff --git a/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImpl.java b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..e540ed49dc2b321c0606fc3ac9e2e4c7607cb842 --- /dev/null +++ b/weather/src/main/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImpl.java @@ -0,0 +1,177 @@ +package cz.muni.fi.pa165.weather.server.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.muni.fi.pa165.core.client.AirportApi; +import cz.muni.fi.pa165.core.client.invoker.ApiException; +import cz.muni.fi.pa165.core.client.model.AirportDto; +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.HourlyWeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.OffsetDateTime; + +@Service +public class WeatherServiceImpl implements WeatherService { + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final AirportApi airportClient = new AirportApi(); + + private static final String TEMPERATURE_ATTRIBUTE_NAME = "temperature_2m"; + private static final String RAIN_ATTRIBUTE_NAME = "rain"; + private static final String WIND_ATTRIBUTE_NAME = "windspeed_10m"; + private static final String VISIBILITY_ATTRIBUTE_NAME = "visibility"; + + @Override + public WeatherForecast getWeatherForecast(double latitude, double longitude) { + String weatherForecastJsonAsString = restTemplate.getForObject( + getHourlyForecastUrl(latitude, longitude), + String.class + ); + try { + HourlyWeatherForecast hourlyWeatherForecast = objectMapper + .readValue(weatherForecastJsonAsString, HourlyWeatherForecast.class); + + return getActualWeatherForecastFromHourlyForecast(hourlyWeatherForecast); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public FlightCreationAdvice isSafeToCreateFlight( + Long departureAirportId, + Long arrivalAirportId, + OffsetDateTime departureTime, + OffsetDateTime arrivalTime + ) { + try { + AirportDto departureAirportDto = airportClient.getAirportById(departureAirportId); + AirportDto arrivalAirportDto = airportClient.getAirportById(arrivalAirportId); + + WeatherForecast departureForecast = getWeatherForecast( + departureAirportDto.getLocation().getLatitude(), + departureAirportDto.getLocation().getLongitude() + ); + WeatherForecast arrivalForecast = getWeatherForecast( + arrivalAirportDto.getLocation().getLatitude(), + arrivalAirportDto.getLocation().getLongitude() + ); + + var flightCreationAdvice = new FlightCreationAdvice(); + flightCreationAdvice.setResult(false); + if (!departureForecast.temperatureInSafeBounds() + || !arrivalForecast.temperatureInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_TEMPERATURE); + } else if (!departureForecast.windSpeedInSafeBounds() || + !arrivalForecast.windSpeedInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_WIND); + } else if (!departureForecast.rainInSafeBounds() || + !arrivalForecast.rainInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_RAIN); + } else if (!departureForecast.visibilityInSafeBounds() || + !arrivalForecast.visibilityInSafeBounds()) { + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_VISIBILITY); + } else { + flightCreationAdvice.setResult(true); + flightCreationAdvice.setWeatherReason(WeatherReason.OK_EVERYTHING); + } + + return flightCreationAdvice; + } catch (ApiException e) { + throw new RuntimeException(e); + } + } + + private WeatherForecast getActualWeatherForecastFromHourlyForecast(HourlyWeatherForecast hourlyWeatherForecast) { + return getWeatherForecastFromHourlyForHour(hourlyWeatherForecast, TimeUtils.getActualHour()); + } + + WeatherForecast getWeatherForecastFromHourlyForHour( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + var weatherForecast = new WeatherForecast(); + weatherForecast.setTemperature(parseTemperature(hourlyWeatherForecast, actualHour)); + weatherForecast.setRain(parseRain(hourlyWeatherForecast, actualHour)); + weatherForecast.setWindSpeed(parseWindSpeed(hourlyWeatherForecast, actualHour)); + weatherForecast.setVisibility(parseVisibility(hourlyWeatherForecast, actualHour)); + + return weatherForecast; + } + + + private double parseValueByAttributeNameAndHour( + HourlyWeatherForecast hourlyWeatherForecast, + String attributeName, + int actualHour + ) { + return (double) hourlyWeatherForecast + .getHourly() + .get(attributeName) + .get(actualHour); + } + + private double parseTemperature( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + TEMPERATURE_ATTRIBUTE_NAME, + actualHour + ); + } + + private double parseRain( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + RAIN_ATTRIBUTE_NAME, + actualHour + ); + } + + private double parseWindSpeed( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + WIND_ATTRIBUTE_NAME, + actualHour + ); + } + + private double parseVisibility( + HourlyWeatherForecast hourlyWeatherForecast, + int actualHour + ) { + return parseValueByAttributeNameAndHour( + hourlyWeatherForecast, + VISIBILITY_ATTRIBUTE_NAME, + actualHour + ); + } + + static String getHourlyForecastUrl( + double latitude, + double longitude + ) { + return "https://api.open-meteo.com/v1/forecast" + + "?latitude=" + latitude + + "&longitude=" + longitude + + "&hourly=" + + TEMPERATURE_ATTRIBUTE_NAME + "," + + RAIN_ATTRIBUTE_NAME + "," + + VISIBILITY_ATTRIBUTE_NAME + "," + + WIND_ATTRIBUTE_NAME + + "&forecast_days=1"; + } +} diff --git a/weather/src/main/resources/application.yml b/weather/src/main/resources/application.yml index 08729559efdd11fb686be558299337798e337ec7..8585c334357fcdc10ebddbaae9586a2cb0467195 100644 --- a/weather/src/main/resources/application.yml +++ b/weather/src/main/resources/application.yml @@ -9,4 +9,24 @@ spring: # Let's make weather microservice run on port 8088 # for now to not collide with another microservices server: - port: 8088 \ No newline at end of file + port: 8088 +management: + endpoints: + web: + exposure: + include: 'info,health,metrics,prometheus' + info: + env: + enabled: true + endpoint: + health: + show-details: always + show-components: always + probes: + enabled: true +info: + app: + encoding: 'UTF-8' + java: + source: '17' + target: '17' diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java index 59dfb1620e3da0b2043a00ac6617d4f6c19c3fbd..78506e354b0c4d58691d4c92a1ab645490b470b2 100644 --- a/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/WeatherApplicationIT.java @@ -1,18 +1,25 @@ package cz.muni.fi.pa165.weather.server; import com.fasterxml.jackson.databind.ObjectMapper; -import cz.muni.fi.pa165.weather.server.model.FlightCreationAdvice; -import cz.muni.fi.pa165.weather.server.model.WeatherForecast; -import cz.muni.fi.pa165.weather.server.model.WeatherReason; +import cz.muni.fi.pa165.weather.server.facade.WeatherFacade; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; +import java.time.OffsetDateTime; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -32,6 +39,9 @@ class WeatherApplicationIT { @Autowired ObjectMapper objectMapper; + @MockBean + private WeatherFacade weatherFacade; + @Test void getWeatherForecastTest() throws Exception { log.debug("getWeatherForecastTest() running"); @@ -39,7 +49,15 @@ class WeatherApplicationIT { var expectedTemperature = 3.14; var expectedWindSpeed = 15.92; var expectedRain = 0.8; - var expectedVisibility = 6.7; + var expectedVisibility = 6000.7; + + when(weatherFacade.getWeatherForecast(4.0, 4.0)) + .thenReturn(new WeatherForecastDto() + .temperature(expectedTemperature) + .windSpeed(expectedWindSpeed) + .rain(expectedRain) + .visibility(expectedVisibility) + ); var response = mockMvc.perform(get("/api/weatherForecast?latitude=4&longitude=4")) .andExpect(status().isOk()) @@ -50,7 +68,10 @@ class WeatherApplicationIT { .andReturn().getResponse().getContentAsString(); log.debug("response: {}", response); - var weatherForecastResponse = objectMapper.readValue(response, WeatherForecast.class); + verify(weatherFacade).getWeatherForecast(4.0, 4.0); + verifyNoMoreInteractions(weatherFacade); + + var weatherForecastResponse = objectMapper.readValue(response, WeatherForecastDto.class); assertThat(weatherForecastResponse.getTemperature()).isEqualTo(expectedTemperature); assertThat(weatherForecastResponse.getWindSpeed()).isEqualTo(expectedWindSpeed); assertThat(weatherForecastResponse.getRain()).isEqualTo(expectedRain); @@ -62,7 +83,22 @@ class WeatherApplicationIT { log.debug("isSafeToCreateFlightTest() running"); var expectedResult = true; - var expectedReason = WeatherReason.OK_EVERYTHING; + var expectedReason = WeatherReasonDto.OK_EVERYTHING; + + String departureTimeAsString = "2012-12-31T22:00:00.000Z"; + String arrivalTimeAsString = "2012-12-31T23:20:00.000Z"; + OffsetDateTime departureTime = OffsetDateTime.parse(departureTimeAsString); + OffsetDateTime arrivalTime = OffsetDateTime.parse(arrivalTimeAsString); + + when(weatherFacade.isSafeToCreateFlight( + 1L, + 2L, + departureTime, + arrivalTime) + ).thenReturn(new FlightCreationAdviceDto() + .result(expectedResult) + .reason(expectedReason) + ); var response = mockMvc.perform(get("/api/isSafeToCreateFlight/1/2?departureTime=2012-12-31T22:00:00.000Z&arrivalTime=2012-12-31T23:20:00.000Z")) .andExpect(status().isOk()) @@ -71,7 +107,10 @@ class WeatherApplicationIT { .andReturn().getResponse().getContentAsString(); log.debug("response: {}", response); - var weatherForecastResponse = objectMapper.readValue(response, FlightCreationAdvice.class); + verify(weatherFacade).isSafeToCreateFlight(1L, 2L, departureTime, arrivalTime); + verifyNoMoreInteractions(weatherFacade); + + var weatherForecastResponse = objectMapper.readValue(response, FlightCreationAdviceDto.class); assertThat(weatherForecastResponse.getResult()).isEqualTo(expectedResult); assertThat(weatherForecastResponse.getReason()).isEqualTo(expectedReason); } diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/data/WeatherForecastTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/data/WeatherForecastTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c0448f5f7544fded911b000fc5e764ebde4a79d8 --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/data/WeatherForecastTest.java @@ -0,0 +1,77 @@ +package cz.muni.fi.pa165.weather.server.data; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class WeatherForecastTest { + + @Autowired + private WeatherForecast weatherForecast; + + @Test + void temperatureInSafeBoundsTrue() { + weatherForecast.setTemperature(21.2); + + assertTrue(weatherForecast.temperatureInSafeBounds()); + } + + @Test + void temperatureTooHigh() { + weatherForecast.setTemperature(42.42); + + assertFalse(weatherForecast.temperatureInSafeBounds()); + } + + @Test + void temperatureTooLow() { + weatherForecast.setTemperature(-42.42); + + assertFalse(weatherForecast.temperatureInSafeBounds()); + } + + @Test + void windSpeedInSafeBoundsTrue() { + weatherForecast.setWindSpeed(13); + + assertTrue(weatherForecast.windSpeedInSafeBounds()); + } + + @Test + void windSpeedTooHigh() { + weatherForecast.setWindSpeed(81); + + assertFalse(weatherForecast.windSpeedInSafeBounds()); + } + + @Test + void rainInSafeBoundsTrue() { + weatherForecast.setRain(1.2); + + assertTrue(weatherForecast.rainInSafeBounds()); + } + + @Test + void rainsTooMuch() { + weatherForecast.setRain(3.2); + + assertFalse(weatherForecast.rainInSafeBounds()); + } + + @Test + void visibilityInSafeBoundsTrue() { + weatherForecast.setVisibility(13500); + + assertTrue(weatherForecast.visibilityInSafeBounds()); + } + + @Test + void visibilityTooLow() { + weatherForecast.setVisibility(911); + + assertFalse(weatherForecast.visibilityInSafeBounds()); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..80bd8d8180214e39fb99d860890e835128080e1a --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/facade/WeatherFacadeTest.java @@ -0,0 +1,88 @@ +package cz.muni.fi.pa165.weather.server.facade; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import cz.muni.fi.pa165.weather.server.service.WeatherService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@SpringBootTest +class WeatherFacadeTest { + + @Autowired + private WeatherFacade weatherFacade; + + @MockBean + private WeatherService weatherService; + + private static final WeatherForecast weatherForecast = new WeatherForecast(); + private static final FlightCreationAdvice flightCreationAdvice = new FlightCreationAdvice(); + + @BeforeEach + void setUp() { + weatherForecast.setTemperature(21.2); + weatherForecast.setWindSpeed(13); + weatherForecast.setRain(1.2); + weatherForecast.setVisibility(13900); + + flightCreationAdvice.setResult(false); + flightCreationAdvice.setWeatherReason(WeatherReason.NOK_WIND); + } + + @Test + void getWeatherForecast() { + when(weatherService.getWeatherForecast(anyDouble(), anyDouble())) + .thenReturn(weatherForecast); + + WeatherForecastDto weatherForecastDto = weatherFacade.getWeatherForecast(42.42, -42.42); + + verify(weatherService) + .getWeatherForecast(42.42, -42.42); + verifyNoMoreInteractions(weatherService); + assertThat(weatherForecastDto.getTemperature()) + .isEqualTo(weatherForecast.getTemperature()); + assertThat(weatherForecastDto.getWindSpeed()) + .isEqualTo(weatherForecast.getWindSpeed()); + assertThat(weatherForecastDto.getRain()) + .isEqualTo(weatherForecast.getRain()); + assertThat(weatherForecastDto.getVisibility()) + .isEqualTo(weatherForecast.getVisibility()); + } + + @Test + void isSafeToCreateFlight() { + var departureTime = OffsetDateTime.parse("2023-04-17T17:42:00Z"); + var arrivalTime = OffsetDateTime.parse("2023-04-17T21:22:00+02:00"); + when(weatherService.isSafeToCreateFlight(1L, 13L, departureTime, arrivalTime)) + .thenReturn(flightCreationAdvice); + + FlightCreationAdviceDto flightCreationAdviceDto = weatherFacade.isSafeToCreateFlight( + 1L, + 13L, + departureTime, + arrivalTime + ); + + verify(weatherService).isSafeToCreateFlight(1L, 13L, departureTime, arrivalTime); + verifyNoMoreInteractions(weatherService); + assertFalse(flightCreationAdviceDto.getResult()); + assertThat(flightCreationAdviceDto.getReason()) + .isEqualTo(WeatherReasonDto.NOK_WIND); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapperTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0d48669d0e1d2f27f5449667a008109e48aec3f2 --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/FlightCreationAdviceMapperTest.java @@ -0,0 +1,32 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.FlightCreationAdvice; +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.FlightCreationAdviceDto; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +class FlightCreationAdviceMapperTest { + + @Autowired + private FlightCreationAdviceMapper flightCreationAdviceMapper; + + @Test + void toDto() { + var flightCreationAdviceEntity = new FlightCreationAdvice(); + flightCreationAdviceEntity.setResult(false); + flightCreationAdviceEntity.setWeatherReason(WeatherReason.NOK_RAIN); + + FlightCreationAdviceDto flightCreationAdviceDto = flightCreationAdviceMapper.toDto(flightCreationAdviceEntity); + + assertFalse(flightCreationAdviceDto.getResult()); + assertThat(flightCreationAdviceDto.getReason()) + .isEqualTo(WeatherReasonDto.NOK_RAIN); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapperTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..28ea90c93bd575a1f26651223889a582dcae1820 --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherForecastMapperTest.java @@ -0,0 +1,36 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import cz.muni.fi.pa165.weather.server.model.WeatherForecastDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class WeatherForecastMapperTest { + + @Autowired + private WeatherForecastMapper weatherForecastMapper; + + @Test + void toDto() { + var weatherForecastEntity = new WeatherForecast(); + weatherForecastEntity.setTemperature(23.1); + weatherForecastEntity.setRain(0.3); + weatherForecastEntity.setWindSpeed(13.3); + weatherForecastEntity.setVisibility(18910); + + WeatherForecastDto weatherForecastDto = weatherForecastMapper.toDto(weatherForecastEntity); + + assertThat(weatherForecastDto.getTemperature()) + .isEqualTo(weatherForecastEntity.getTemperature()); + assertThat(weatherForecastDto.getRain()) + .isEqualTo(weatherForecastEntity.getRain()); + assertThat(weatherForecastDto.getWindSpeed()) + .isEqualTo(weatherForecastEntity.getWindSpeed()); + assertThat(weatherForecastDto.getVisibility()) + .isEqualTo(weatherForecastEntity.getVisibility()); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapperTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9320207b6a52d7f5b4aed1d06e85d1bd6a2d59cc --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/mapper/WeatherReasonMapperTest.java @@ -0,0 +1,36 @@ +package cz.muni.fi.pa165.weather.server.mapper; + +import cz.muni.fi.pa165.weather.server.data.WeatherReason; +import cz.muni.fi.pa165.weather.server.model.WeatherReasonDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class WeatherReasonMapperTest { + + @Autowired + private WeatherReasonMapper weatherReasonMapper; + + @Test + void toDtoWhenOkEverything() { + var weatherReasonEntity = WeatherReason.OK_EVERYTHING; + + WeatherReasonDto weatherReasonDto = weatherReasonMapper.toDto(weatherReasonEntity); + + assertThat(weatherReasonDto) + .isEqualTo(WeatherReasonDto.OK_EVERYTHING); + } + + @Test + void toDtoWhenNokWind() { + var weatherReasonEntity = WeatherReason.NOK_WIND; + + WeatherReasonDto weatherReasonDto = weatherReasonMapper.toDto(weatherReasonEntity); + + assertThat(weatherReasonDto) + .isEqualTo(WeatherReasonDto.NOK_WIND); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/TimeUtilsTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/TimeUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..711106820068c9ecc81622e89859b3380496a19d --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/TimeUtilsTest.java @@ -0,0 +1,59 @@ +package cz.muni.fi.pa165.weather.server.service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.time.Month; +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class TimeUtilsTest { + + @Test + void toLocalDateTimeFromUTC() { + OffsetDateTime offsetDateTime = OffsetDateTime + .parse("2023-04-17T17:42:18Z"); + + LocalDateTime localDateTime = TimeUtils.toLocalDateTime(offsetDateTime); + + assertThat(localDateTime) + .isEqualTo(LocalDateTime.of(2023, Month.APRIL, 17, 19, 42, 18)); + } + + @Test + void toLocalDateTimeFromPositiveTimeZone() { + OffsetDateTime offsetDateTime = OffsetDateTime + .parse("2014-09-04T21:18:13+01:00"); + + LocalDateTime localDateTime = TimeUtils.toLocalDateTime(offsetDateTime); + + assertThat(localDateTime) + .isEqualTo(LocalDateTime.of(2014, Month.SEPTEMBER, 4, 22, 18, 13)); + } + + @Test + void toLocalDateTimeFromNegativeZone() { + OffsetDateTime offsetDateTime = OffsetDateTime + .parse("2023-05-05T13:09:42-02:00"); + + LocalDateTime localDateTime = TimeUtils.toLocalDateTime(offsetDateTime); + + assertThat(localDateTime) + .isEqualTo(LocalDateTime.of(2023, Month.MAY, 5, 17, 9, 42)); + } + + @Test + void getHourIndexOf() { + LocalDateTime localDateTime = LocalDateTime.of( + 2023, Month.APRIL, 17, 14, 42 + ); + + int hourIndex = TimeUtils.getHourOf(localDateTime); + + assertThat(hourIndex) + .isEqualTo(14); + } +} \ No newline at end of file diff --git a/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImplTest.java b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cba9a4c300ac0f6359fcb632211f146695440f8c --- /dev/null +++ b/weather/src/test/java/cz/muni/fi/pa165/weather/server/service/WeatherServiceImplTest.java @@ -0,0 +1,70 @@ +package cz.muni.fi.pa165.weather.server.service; + +import cz.muni.fi.pa165.weather.server.data.HourlyWeatherForecast; +import cz.muni.fi.pa165.weather.server.data.WeatherForecast; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class WeatherServiceImplTest { + + private static final HourlyWeatherForecast hourlyWeatherForecast = new HourlyWeatherForecast(); + + @Autowired + private WeatherServiceImpl weatherService; + + @BeforeEach + void setUp() { + hourlyWeatherForecast.setHourly(Map.ofEntries( + Map.entry("temperature_2m", List.of(14.2, -21.3, 12.8)), + Map.entry("rain", List.of(0.9, 0.0, 0.1)), + Map.entry("windspeed_10m", List.of(31.2, 29.1, 42.42)), + Map.entry("visibility", List.of(12800.0, 13200.0, 5500.0)) + )); + } + + @Test + void getWeatherForecastFromHourlyForHour0() { + WeatherForecast weatherForecast = weatherService + .getWeatherForecastFromHourlyForHour(hourlyWeatherForecast, 0); + + assertThat(weatherForecast.getTemperature()) + .isEqualTo(14.2); + assertThat(weatherForecast.getRain()) + .isEqualTo(0.9); + assertThat(weatherForecast.getWindSpeed()) + .isEqualTo(31.2); + assertThat(weatherForecast.getVisibility()) + .isEqualTo(12800.0); + } + + @Test + void getWeatherForecastFromHourlyForHour1() { + WeatherForecast weatherForecast = weatherService + .getWeatherForecastFromHourlyForHour(hourlyWeatherForecast, 1); + + assertThat(weatherForecast.getTemperature()) + .isEqualTo(-21.3); + assertThat(weatherForecast.getRain()) + .isEqualTo(0.0); + assertThat(weatherForecast.getWindSpeed()) + .isEqualTo(29.1); + assertThat(weatherForecast.getVisibility()) + .isEqualTo(13200.0); + } + + @Test + void getHourlyForecastUrl() { + String hourlyForecastUrl = WeatherServiceImpl.getHourlyForecastUrl(42.42, -42.42); + + assertThat(hourlyForecastUrl) + .isEqualTo("https://api.open-meteo.com/v1/forecast?latitude=42.42&longitude=-42.42&hourly=temperature_2m,rain,visibility,windspeed_10m&forecast_days=1"); + } +} \ No newline at end of file