Switched Weather Provider to OpenMeteo #15
This commit is contained in:
parent
5f85417c80
commit
5df4da86a5
3 changed files with 228 additions and 353 deletions
|
|
@ -18,13 +18,14 @@ import org.springframework.web.client.ResourceAccessException;
|
|||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
|
|
@ -49,47 +50,26 @@ class WeatherServiceTest {
|
|||
private Activity testActivity;
|
||||
private UUID activityId;
|
||||
|
||||
// Sample OpenWeatherMap API response
|
||||
// Sample Open-Meteo archive API response (clear sky, WMO code 0)
|
||||
private static final String SAMPLE_WEATHER_RESPONSE = """
|
||||
{
|
||||
"coord": {"lon": 8.2552, "lat": 49.9894},
|
||||
"weather": [
|
||||
{
|
||||
"id": 800,
|
||||
"main": "Clear",
|
||||
"description": "clear sky",
|
||||
"icon": "01d"
|
||||
}
|
||||
],
|
||||
"base": "stations",
|
||||
"main": {
|
||||
"temp": 15.5,
|
||||
"feels_like": 14.2,
|
||||
"temp_min": 13.0,
|
||||
"temp_max": 17.0,
|
||||
"pressure": 1013,
|
||||
"humidity": 65
|
||||
},
|
||||
"visibility": 10000,
|
||||
"wind": {
|
||||
"speed": 3.5,
|
||||
"deg": 180
|
||||
},
|
||||
"clouds": {
|
||||
"all": 20
|
||||
},
|
||||
"dt": 1700758089,
|
||||
"sys": {
|
||||
"type": 2,
|
||||
"id": 2012516,
|
||||
"country": "DE",
|
||||
"sunrise": 1700721600,
|
||||
"sunset": 1700757600
|
||||
},
|
||||
"timezone": 3600,
|
||||
"id": 2873891,
|
||||
"name": "Mannheim",
|
||||
"cod": 200
|
||||
"latitude": 49.98,
|
||||
"longitude": 8.26,
|
||||
"hourly": {
|
||||
"time": ["2025-11-23T17:00", "2025-11-23T18:00", "2025-11-23T19:00"],
|
||||
"temperature_2m": [14.0, 15.5, 13.8],
|
||||
"apparent_temperature": [12.5, 14.2, 12.0],
|
||||
"relative_humidity_2m": [60, 65, 70],
|
||||
"surface_pressure": [1012, 1013, 1012],
|
||||
"wind_speed_10m": [10.0, 12.6, 11.0],
|
||||
"wind_direction_10m": [170, 180, 190],
|
||||
"cloud_cover": [15, 20, 25],
|
||||
"rain": [0.0, 0.0, 0.0],
|
||||
"snowfall": [0.0, 0.0, 0.0],
|
||||
"precipitation": [0.0, 0.0, 0.0],
|
||||
"visibility": [10000, 10000, 10000],
|
||||
"weather_code": [0, 0, 1]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
|
|
@ -98,12 +78,11 @@ class WeatherServiceTest {
|
|||
activityId = UUID.randomUUID();
|
||||
testActivity = new Activity();
|
||||
testActivity.setId(activityId);
|
||||
testActivity.setStartedAt(LocalDateTime.now().minusDays(1)); // Recent activity
|
||||
testActivity.setStartedAt(LocalDateTime.of(2025, 11, 23, 18, 8, 9));
|
||||
|
||||
// Inject the real RestTemplate mock and set config values
|
||||
ReflectionTestUtils.setField(weatherService, "restTemplate", restTemplate);
|
||||
ReflectionTestUtils.setField(weatherService, "weatherEnabled", true);
|
||||
ReflectionTestUtils.setField(weatherService, "apiKey", "test-api-key-12345678901234567890");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -123,7 +102,7 @@ class WeatherServiceTest {
|
|||
""";
|
||||
testActivity.setTrackPointsJson(trackPointsJson);
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenReturn(SAMPLE_WEATHER_RESPONSE);
|
||||
when(weatherDataRepository.save(any(WeatherData.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
|
@ -136,7 +115,7 @@ class WeatherServiceTest {
|
|||
assertEquals(new BigDecimal("15.5"), weatherData.getTemperatureCelsius());
|
||||
assertEquals("Clear", weatherData.getWeatherCondition());
|
||||
|
||||
verify(restTemplate, times(1)).getForObject(any(URI.class), eq(String.class));
|
||||
verify(restTemplate, times(1)).getForObject(anyString(), eq(String.class), any(Map.class));
|
||||
verify(weatherDataRepository, times(1)).save(any(WeatherData.class));
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +141,7 @@ class WeatherServiceTest {
|
|||
""";
|
||||
testActivity.setTrackPointsJson(trackPointsJson);
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenReturn(SAMPLE_WEATHER_RESPONSE);
|
||||
when(weatherDataRepository.save(any(WeatherData.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
|
@ -176,39 +155,13 @@ class WeatherServiceTest {
|
|||
assertEquals(new BigDecimal("14.2"), weatherData.getFeelsLikeCelsius());
|
||||
assertEquals(65, weatherData.getHumidity());
|
||||
assertEquals(1013, weatherData.getPressure());
|
||||
assertEquals(new BigDecimal("3.5"), weatherData.getWindSpeedMps());
|
||||
assertEquals(new BigDecimal("3.50"), weatherData.getWindSpeedMps());
|
||||
assertEquals("clear sky", weatherData.getWeatherDescription());
|
||||
|
||||
verify(restTemplate, times(1)).getForObject(any(URI.class), eq(String.class));
|
||||
verify(restTemplate, times(1)).getForObject(anyString(), eq(String.class), any(Map.class));
|
||||
verify(weatherDataRepository, times(1)).save(any(WeatherData.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty when weather is disabled in config")
|
||||
void testFetchWeather_Disabled() {
|
||||
ReflectionTestUtils.setField(weatherService, "weatherEnabled", false);
|
||||
testActivity.setTrackPointsJson("[{\"lat\":50.0,\"lon\":8.0}]");
|
||||
|
||||
Optional<WeatherData> result = weatherService.fetchWeatherForActivity(testActivity);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
verify(weatherDataRepository, never()).save(any(WeatherData.class));
|
||||
verify(restTemplate, never()).getForObject(any(URI.class), eq(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty when API key is not configured")
|
||||
void testFetchWeather_NoApiKey() {
|
||||
ReflectionTestUtils.setField(weatherService, "apiKey", "");
|
||||
testActivity.setTrackPointsJson("[{\"lat\":50.0,\"lon\":8.0}]");
|
||||
|
||||
Optional<WeatherData> result = weatherService.fetchWeatherForActivity(testActivity);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
verify(weatherDataRepository, never()).save(any(WeatherData.class));
|
||||
verify(restTemplate, never()).getForObject(any(URI.class), eq(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty when track points JSON is null")
|
||||
void testFetchWeather_NoTrackPoints() {
|
||||
|
|
@ -260,20 +213,7 @@ class WeatherServiceTest {
|
|||
|
||||
assertTrue(result.isEmpty());
|
||||
verify(weatherDataRepository, never()).save(any(WeatherData.class));
|
||||
verify(restTemplate, never()).getForObject(any(URI.class), eq(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return empty for old activities (>5 days)")
|
||||
void testFetchWeather_OldActivity() {
|
||||
testActivity.setStartedAt(LocalDateTime.now().minusDays(10)); // Old activity
|
||||
testActivity.setTrackPointsJson("[{\"latitude\":50.0,\"longitude\":8.0}]");
|
||||
|
||||
Optional<WeatherData> result = weatherService.fetchWeatherForActivity(testActivity);
|
||||
|
||||
assertTrue(result.isEmpty());
|
||||
verify(restTemplate, never()).getForObject(any(URI.class), eq(String.class));
|
||||
verify(weatherDataRepository, never()).save(any(WeatherData.class));
|
||||
verify(restTemplate, never()).getForObject(anyString(), eq(String.class), any(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -281,11 +221,11 @@ class WeatherServiceTest {
|
|||
void testFetchWeather_AuthenticationError() {
|
||||
testActivity.setTrackPointsJson("[{\"latitude\":50.0,\"longitude\":8.0}]");
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenThrow(new HttpClientErrorException(
|
||||
org.springframework.http.HttpStatus.UNAUTHORIZED,
|
||||
"Unauthorized",
|
||||
"{\"cod\":401,\"message\":\"Invalid API key\"}".getBytes(),
|
||||
"{\"error\":true,\"reason\":\"Unauthorized\"}".getBytes(),
|
||||
null
|
||||
));
|
||||
|
||||
|
|
@ -300,7 +240,7 @@ class WeatherServiceTest {
|
|||
void testFetchWeather_NetworkError() {
|
||||
testActivity.setTrackPointsJson("[{\"latitude\":50.0,\"longitude\":8.0}]");
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenThrow(new ResourceAccessException("Connection timeout"));
|
||||
|
||||
Optional<WeatherData> result = weatherService.fetchWeatherForActivity(testActivity);
|
||||
|
|
@ -314,7 +254,7 @@ class WeatherServiceTest {
|
|||
void testFetchWeather_MalformedResponse() {
|
||||
testActivity.setTrackPointsJson("[{\"latitude\":50.0,\"longitude\":8.0}]");
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenReturn("this is not valid JSON");
|
||||
|
||||
Optional<WeatherData> result = weatherService.fetchWeatherForActivity(testActivity);
|
||||
|
|
@ -328,24 +268,29 @@ class WeatherServiceTest {
|
|||
void testParseWeatherResponse_AllFields() {
|
||||
String responseWithRain = """
|
||||
{
|
||||
"main": {
|
||||
"temp": 10.0,
|
||||
"feels_like": 8.5,
|
||||
"pressure": 1015,
|
||||
"humidity": 80
|
||||
},
|
||||
"weather": [{"main": "Rain", "description": "light rain", "icon": "10d"}],
|
||||
"wind": {"speed": 5.5, "deg": 270},
|
||||
"clouds": {"all": 75},
|
||||
"visibility": 8000,
|
||||
"rain": {"1h": 2.5},
|
||||
"sys": {"sunrise": 1700721600, "sunset": 1700757600}
|
||||
"latitude": 50.0,
|
||||
"longitude": 8.0,
|
||||
"hourly": {
|
||||
"time": ["2025-11-23T17:00", "2025-11-23T18:00", "2025-11-23T19:00"],
|
||||
"temperature_2m": [11.0, 10.0, 9.5],
|
||||
"apparent_temperature": [9.0, 8.5, 8.0],
|
||||
"relative_humidity_2m": [75, 80, 85],
|
||||
"surface_pressure": [1014, 1015, 1015],
|
||||
"wind_speed_10m": [18.0, 19.8, 20.0],
|
||||
"wind_direction_10m": [260, 270, 275],
|
||||
"cloud_cover": [70, 75, 80],
|
||||
"rain": [1.5, 2.5, 3.0],
|
||||
"snowfall": [0.0, 0.0, 0.0],
|
||||
"precipitation": [1.5, 2.5, 3.0],
|
||||
"visibility": [9000, 8000, 7000],
|
||||
"weather_code": [61, 61, 63]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
testActivity.setTrackPointsJson("[{\"latitude\":50.0,\"longitude\":8.0}]");
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenReturn(responseWithRain);
|
||||
when(weatherDataRepository.save(any(WeatherData.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
|
@ -357,15 +302,13 @@ class WeatherServiceTest {
|
|||
assertEquals(new BigDecimal("10.0"), weatherData.getTemperatureCelsius());
|
||||
assertEquals(new BigDecimal("8.5"), weatherData.getFeelsLikeCelsius());
|
||||
assertEquals("Rain", weatherData.getWeatherCondition());
|
||||
assertEquals("light rain", weatherData.getWeatherDescription());
|
||||
assertEquals(new BigDecimal("5.5"), weatherData.getWindSpeedMps());
|
||||
assertEquals("slight rain", weatherData.getWeatherDescription());
|
||||
assertEquals(new BigDecimal("5.50"), weatherData.getWindSpeedMps());
|
||||
assertEquals(270, weatherData.getWindDirection());
|
||||
assertEquals(75, weatherData.getCloudiness());
|
||||
assertEquals(8000, weatherData.getVisibilityMeters());
|
||||
assertEquals(new BigDecimal("2.5"), weatherData.getPrecipitationMm());
|
||||
assertNotNull(weatherData.getSunrise());
|
||||
assertNotNull(weatherData.getSunset());
|
||||
assertEquals("openweathermap", weatherData.getDataSource());
|
||||
assertEquals("open-meteo", weatherData.getDataSource());
|
||||
assertNotNull(weatherData.getFetchedAt());
|
||||
}
|
||||
|
||||
|
|
@ -374,19 +317,29 @@ class WeatherServiceTest {
|
|||
void testParseWeatherResponse_MinimalFields() {
|
||||
String minimalResponse = """
|
||||
{
|
||||
"main": {
|
||||
"temp": 15.0,
|
||||
"feels_like": 14.0,
|
||||
"pressure": 1010,
|
||||
"humidity": 60
|
||||
},
|
||||
"weather": [{"main": "Clouds", "description": "few clouds", "icon": "02d"}]
|
||||
"latitude": 50.0,
|
||||
"longitude": 8.0,
|
||||
"hourly": {
|
||||
"time": ["2025-11-23T18:00"],
|
||||
"temperature_2m": [15.0],
|
||||
"apparent_temperature": [14.0],
|
||||
"relative_humidity_2m": [60],
|
||||
"surface_pressure": [1010],
|
||||
"wind_speed_10m": [null],
|
||||
"wind_direction_10m": [null],
|
||||
"cloud_cover": [null],
|
||||
"rain": [null],
|
||||
"snowfall": [null],
|
||||
"precipitation": [null],
|
||||
"visibility": [null],
|
||||
"weather_code": [2]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
testActivity.setTrackPointsJson("[{\"latitude\":50.0,\"longitude\":8.0}]");
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenReturn(minimalResponse);
|
||||
when(weatherDataRepository.save(any(WeatherData.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
|
@ -441,7 +394,7 @@ class WeatherServiceTest {
|
|||
""";
|
||||
testActivity.setTrackPointsJson(trackPointsJson);
|
||||
|
||||
when(restTemplate.getForObject(any(URI.class), eq(String.class)))
|
||||
when(restTemplate.getForObject(anyString(), eq(String.class), any(Map.class)))
|
||||
.thenReturn(SAMPLE_WEATHER_RESPONSE);
|
||||
when(weatherDataRepository.save(any(WeatherData.class)))
|
||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
|
@ -449,6 +402,6 @@ class WeatherServiceTest {
|
|||
Optional<WeatherData> result = weatherService.fetchWeatherForActivity(testActivity);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
verify(restTemplate, times(1)).getForObject(any(URI.class), eq(String.class));
|
||||
verify(restTemplate, times(1)).getForObject(anyString(), eq(String.class), any(Map.class));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue