diff --git a/metarParser-services/pom.xml b/metarParser-services/pom.xml
index 266c9e03..6472967b 100644
--- a/metarParser-services/pom.xml
+++ b/metarParser-services/pom.xml
@@ -12,9 +12,9 @@
metarParser-services
- 0.94
+ 0.93
1
- 0.90
+ 0.88
diff --git a/metarParser-services/src/main/java/io/github/mivek/service/provider/AbstractWeatherProvider.java b/metarParser-services/src/main/java/io/github/mivek/service/provider/AbstractWeatherProvider.java
index 060d0ddd..4ed55f63 100644
--- a/metarParser-services/src/main/java/io/github/mivek/service/provider/AbstractWeatherProvider.java
+++ b/metarParser-services/src/main/java/io/github/mivek/service/provider/AbstractWeatherProvider.java
@@ -23,10 +23,49 @@ public abstract class AbstractWeatherProvider implements WeatherProvider {
/** The required length of a valid ICAO code. */
static final int ICAO_LENGTH = 4;
+ /** The default User-Agent string sent with every HTTP request. */
+ static final String DEFAULT_USER_AGENT = "MetarParser";
+
+ /** The User-Agent string sent with every HTTP request. */
+ private final String userAgent;
+
+ /** The HTTP client used to execute requests. */
+ private final HttpClient httpClient;
+
/**
- * Protected constructor.
+ * Protected constructor. Uses the default User-Agent and a default {@link HttpClient}.
*/
protected AbstractWeatherProvider() {
+ this(DEFAULT_USER_AGENT, HttpClient.newBuilder().build());
+ }
+
+ /**
+ * Protected constructor with a custom User-Agent string and a default {@link HttpClient}.
+ *
+ * @param userAgent the User-Agent header value to send with every HTTP request.
+ */
+ protected AbstractWeatherProvider(final String userAgent) {
+ this(userAgent, HttpClient.newBuilder().build());
+ }
+
+ /**
+ * Package-private constructor for testing. Accepts an injectable {@link HttpClient}.
+ *
+ * @param httpClient the HTTP client to use for all requests.
+ */
+ AbstractWeatherProvider(final HttpClient httpClient) {
+ this(DEFAULT_USER_AGENT, httpClient);
+ }
+
+ /**
+ * Private canonical constructor used by all other constructors.
+ *
+ * @param userAgent the User-Agent header value to send with every HTTP request.
+ * @param httpClient the HTTP client to use for all requests.
+ */
+ private AbstractWeatherProvider(final String userAgent, final HttpClient httpClient) {
+ this.userAgent = userAgent;
+ this.httpClient = httpClient;
}
/**
@@ -42,7 +81,7 @@ protected final void checkIcao(final String icao) throws ParseException {
}
/**
- * Builds an HTTP GET request for the given URL.
+ * Builds an HTTP GET request for the given URL, including the configured {@code User-Agent} header.
*
* @param url the URL to request.
* @return the constructed {@link HttpRequest}.
@@ -53,6 +92,7 @@ protected final HttpRequest buildRequest(final String url) throws URISyntaxExcep
.uri(new URI(url))
.GET()
.version(HttpClient.Version.HTTP_2)
+ .header("User-Agent", userAgent)
.build();
}
@@ -70,9 +110,7 @@ protected final HttpRequest buildRequest(final String url) throws URISyntaxExcep
protected final HttpResponse> getHttpResponse(final String url)
throws IOException, URISyntaxException, InterruptedException, ParseException {
HttpRequest request = buildRequest(url);
- HttpResponse> response = HttpClient.newBuilder()
- .build()
- .send(request, HttpResponse.BodyHandlers.ofLines());
+ HttpResponse> response = httpClient.send(request, HttpResponse.BodyHandlers.ofLines());
if (response.statusCode() != HttpURLConnection.HTTP_OK) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_ICAO);
}
diff --git a/metarParser-services/src/main/java/io/github/mivek/service/provider/AviationWeatherProvider.java b/metarParser-services/src/main/java/io/github/mivek/service/provider/AviationWeatherProvider.java
index 91aa71d0..95e26332 100644
--- a/metarParser-services/src/main/java/io/github/mivek/service/provider/AviationWeatherProvider.java
+++ b/metarParser-services/src/main/java/io/github/mivek/service/provider/AviationWeatherProvider.java
@@ -35,6 +35,31 @@ public final class AviationWeatherProvider extends AbstractWeatherProvider {
/** The "SPECI " report-type prefix returned by the API. */
private static final String SPECI_PREFIX = "SPECI ";
+ /**
+ * Default constructor. Uses the default User-Agent.
+ */
+ public AviationWeatherProvider() {
+ super();
+ }
+
+ /**
+ * Constructor with a custom User-Agent string.
+ *
+ * @param userAgent the User-Agent header value to send with every HTTP request.
+ */
+ public AviationWeatherProvider(final String userAgent) {
+ super(userAgent);
+ }
+
+ /**
+ * Package-private constructor for testing. Accepts an injectable {@link java.net.http.HttpClient}.
+ *
+ * @param httpClient the HTTP client to use for all requests.
+ */
+ AviationWeatherProvider(final java.net.http.HttpClient httpClient) {
+ super(httpClient);
+ }
+
@Override
public String retrieveMetar(final String icao) throws ParseException, IOException, URISyntaxException, InterruptedException {
checkIcao(icao);
diff --git a/metarParser-services/src/main/java/io/github/mivek/service/provider/NOAAWeatherProvider.java b/metarParser-services/src/main/java/io/github/mivek/service/provider/NOAAWeatherProvider.java
index b7ec1daf..272a601e 100644
--- a/metarParser-services/src/main/java/io/github/mivek/service/provider/NOAAWeatherProvider.java
+++ b/metarParser-services/src/main/java/io/github/mivek/service/provider/NOAAWeatherProvider.java
@@ -35,6 +35,31 @@ public final class NOAAWeatherProvider extends AbstractWeatherProvider {
/** The AMD TAF token that appears as a second line in amended NOAA TAF responses. */
private static final String AMD_TAF_TOKEN = "AMD TAF";
+ /**
+ * Default constructor. Uses the default User-Agent.
+ */
+ public NOAAWeatherProvider() {
+ super();
+ }
+
+ /**
+ * Constructor with a custom User-Agent string.
+ *
+ * @param userAgent the User-Agent header value to send with every HTTP request.
+ */
+ public NOAAWeatherProvider(final String userAgent) {
+ super(userAgent);
+ }
+
+ /**
+ * Package-private constructor for testing. Accepts an injectable {@link java.net.http.HttpClient}.
+ *
+ * @param httpClient the HTTP client to use for all requests.
+ */
+ NOAAWeatherProvider(final java.net.http.HttpClient httpClient) {
+ super(httpClient);
+ }
+
@Override
public String retrieveMetar(final String icao) throws ParseException, IOException, URISyntaxException, InterruptedException {
checkIcao(icao);
diff --git a/metarParser-services/src/test/java/io/github/mivek/service/AbstractWeatherCodeServiceTest.java b/metarParser-services/src/test/java/io/github/mivek/service/AbstractWeatherCodeServiceTest.java
index 82293943..df63a703 100644
--- a/metarParser-services/src/test/java/io/github/mivek/service/AbstractWeatherCodeServiceTest.java
+++ b/metarParser-services/src/test/java/io/github/mivek/service/AbstractWeatherCodeServiceTest.java
@@ -5,9 +5,6 @@
import io.github.mivek.model.AbstractWeatherCode;
import org.junit.jupiter.api.Test;
-import java.io.IOException;
-import java.net.URISyntaxException;
-
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
@@ -15,24 +12,24 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
abstract class AbstractWeatherCodeServiceTest {
- protected abstract AbstractWeatherCodeService getService();
+ protected abstract AbstractWeatherCodeService getService(FakeWeatherProvider provider);
@Test
void testRetrieveFromAirportInvalid() {
- ParseException e = assertThrows(ParseException.class, () -> getService().retrieveFromAirport("RandomIcao"));
+ ParseException e = assertThrows(ParseException.class, () -> getService(new FakeWeatherProvider()).retrieveFromAirport("RandomIcao"));
assertEquals(ErrorCodes.ERROR_CODE_INVALID_ICAO, e.getErrorCode());
}
@Test
- void testRetrieveFromAirport() throws IOException, ParseException, URISyntaxException, InterruptedException {
- T res = getService().retrieveFromAirport("LFPG");
+ void testRetrieveFromAirport() throws Exception {
+ T res = getService(new FakeWeatherProvider()).retrieveFromAirport("LFPG");
assertThat(res, notNullValue());
assertThat(res.getAirport().getIcao(), is("LFPG"));
}
@Test
void testRetrieveFromAirportNotFound() {
- ParseException e = assertThrows(ParseException.class, () -> getService().retrieveFromAirport("lftm"));
+ ParseException e = assertThrows(ParseException.class, () -> getService(new FakeWeatherProvider()).retrieveFromAirport("lftm"));
assertEquals(ErrorCodes.ERROR_CODE_INVALID_ICAO, e.getErrorCode());
}
}
diff --git a/metarParser-services/src/test/java/io/github/mivek/service/FakeWeatherProvider.java b/metarParser-services/src/test/java/io/github/mivek/service/FakeWeatherProvider.java
new file mode 100644
index 00000000..0cb14900
--- /dev/null
+++ b/metarParser-services/src/test/java/io/github/mivek/service/FakeWeatherProvider.java
@@ -0,0 +1,40 @@
+package io.github.mivek.service;
+
+import io.github.mivek.exception.ErrorCodes;
+import io.github.mivek.exception.ParseException;
+import io.github.mivek.service.provider.WeatherProvider;
+
+/**
+ * Test-only {@link WeatherProvider} implementation that returns hardcoded weather strings.
+ * Used to avoid real HTTP calls in service-layer tests.
+ */
+final class FakeWeatherProvider implements WeatherProvider {
+
+ /** Hardcoded METAR string for LFPG. */
+ static final String LFPG_METAR = "LFPG 251830Z 17013KT 9999 OVC006 04/03 Q1012 NOSIG";
+
+ /** Hardcoded TAF string for LFPG. */
+ static final String LFPG_TAF = "TAF LFPG 121700Z 1218/1324 13003KT CAVOK TX09/1315Z TN00/1306Z\nTEMPO 1303/1308 4000 BR";
+
+ @Override
+ public String retrieveMetar(final String icao) throws ParseException {
+ if (icao.length() != 4) {
+ throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_ICAO);
+ }
+ if (!"LFPG".equalsIgnoreCase(icao)) {
+ throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_ICAO);
+ }
+ return LFPG_METAR;
+ }
+
+ @Override
+ public String retrieveTaf(final String icao) throws ParseException {
+ if (icao.length() != 4) {
+ throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_ICAO);
+ }
+ if (!"LFPG".equalsIgnoreCase(icao)) {
+ throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_ICAO);
+ }
+ return LFPG_TAF;
+ }
+}
diff --git a/metarParser-services/src/test/java/io/github/mivek/service/MetarServiceTest.java b/metarParser-services/src/test/java/io/github/mivek/service/MetarServiceTest.java
index 6693dfb8..e6cef426 100644
--- a/metarParser-services/src/test/java/io/github/mivek/service/MetarServiceTest.java
+++ b/metarParser-services/src/test/java/io/github/mivek/service/MetarServiceTest.java
@@ -3,12 +3,8 @@
import io.github.mivek.exception.ParseException;
import io.github.mivek.internationalization.Messages;
import io.github.mivek.model.Metar;
-import io.github.mivek.service.provider.AviationWeatherProvider;
import org.junit.jupiter.api.Test;
-import java.io.IOException;
-import java.net.URISyntaxException;
-
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
@@ -45,15 +41,8 @@ void testDecodeValidMetar() throws ParseException {
}
- @Test
- void testRetrieveFromAirportWithAviationWeatherProvider() throws ParseException, IOException, URISyntaxException, InterruptedException {
- Metar result = MetarService.withProvider(new AviationWeatherProvider()).retrieveFromAirport("LFPG");
- assertNotNull(result);
- assertEquals("LFPG", result.getAirport().getIcao());
- }
-
@Override
- protected AbstractWeatherCodeService getService() {
- return MetarService.getInstance();
+ protected AbstractWeatherCodeService getService(final FakeWeatherProvider provider) {
+ return MetarService.withProvider(provider);
}
}
diff --git a/metarParser-services/src/test/java/io/github/mivek/service/TAFServiceTest.java b/metarParser-services/src/test/java/io/github/mivek/service/TAFServiceTest.java
index 470fa349..9a8bb2f1 100644
--- a/metarParser-services/src/test/java/io/github/mivek/service/TAFServiceTest.java
+++ b/metarParser-services/src/test/java/io/github/mivek/service/TAFServiceTest.java
@@ -1,27 +1,20 @@
package io.github.mivek.service;
-import io.github.mivek.exception.ParseException;
import io.github.mivek.model.TAF;
-import io.github.mivek.service.provider.AviationWeatherProvider;
import org.junit.jupiter.api.Test;
-import java.io.IOException;
-import java.net.URISyntaxException;
-
import static org.junit.jupiter.api.Assertions.assertNotNull;
class TAFServiceTest extends AbstractWeatherCodeServiceTest {
- private final TAFService sut = TAFService.getInstance();
-
@Override
- protected AbstractWeatherCodeService getService() {
- return sut;
+ protected AbstractWeatherCodeService getService(final FakeWeatherProvider provider) {
+ return TAFService.withProvider(provider);
}
@Test
- void testRetrieveFromAirportWithAviationWeatherProvider() throws ParseException, IOException, URISyntaxException, InterruptedException {
- TAF result = TAFService.withProvider(new AviationWeatherProvider()).retrieveFromAirport("LFPG");
+ void testRetrieveFromAirportWithFakeProvider() throws Exception {
+ TAF result = TAFService.withProvider(new FakeWeatherProvider()).retrieveFromAirport("LFPG");
assertNotNull(result);
assertNotNull(result.getAirport());
}
diff --git a/metarParser-services/src/test/java/io/github/mivek/service/provider/AviationWeatherProviderTest.java b/metarParser-services/src/test/java/io/github/mivek/service/provider/AviationWeatherProviderTest.java
index a8fb9e37..d0e7f1e9 100644
--- a/metarParser-services/src/test/java/io/github/mivek/service/provider/AviationWeatherProviderTest.java
+++ b/metarParser-services/src/test/java/io/github/mivek/service/provider/AviationWeatherProviderTest.java
@@ -2,19 +2,43 @@
import io.github.mivek.exception.ErrorCodes;
import io.github.mivek.exception.ParseException;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
class AviationWeatherProviderTest {
- private final AviationWeatherProvider sut = new AviationWeatherProvider();
+ private HttpClient mockHttpClient;
+ private HttpResponse> mockResponse = mock(HttpResponse.class);
+ private AviationWeatherProvider sut;
+
+ @BeforeEach
+ void setUp() {
+ mockHttpClient = mock(HttpClient.class);
+ mockResponse = mock(HttpResponse.class);
+ sut = new AviationWeatherProvider(mockHttpClient);
+ }
+
+ @Test
+ void testConstructors() {
+ assertNotNull(new AviationWeatherProvider());
+ assertNotNull(new AviationWeatherProvider("Custom-Agent"));
+ }
@Test
void testRetrieveMetarInvalidIcao() {
@@ -23,14 +47,32 @@ void testRetrieveMetarInvalidIcao() {
}
@Test
- void testRetrieveMetarNotFound() {
+ void testRetrieveMetarNotFound() throws IOException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(404);
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
ParseException e = assertThrows(ParseException.class, () -> sut.retrieveMetar("lftm"));
assertEquals(ErrorCodes.ERROR_CODE_INVALID_ICAO, e.getErrorCode());
}
+ @Test
+ void testRetrieveMetarEmptyBody() throws IOException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn(Stream.empty());
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
+ ParseException e = assertThrows(ParseException.class, () -> sut.retrieveMetar("LFPG"));
+ assertEquals(ErrorCodes.ERROR_CODE_INVALID_ICAO, e.getErrorCode());
+ }
+
@Test
void testRetrieveMetar() throws ParseException, IOException, URISyntaxException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn(Stream.of("LFPG 251830Z 17013KT 9999 OVC006 04/03 Q1012 NOSIG"));
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
String result = sut.retrieveMetar("LFPG");
+
assertNotNull(result);
assertTrue(result.contains("LFPG"));
}
@@ -42,14 +84,24 @@ void testRetrieveTafInvalidIcao() {
}
@Test
- void testRetrieveTafNotFound() {
+ void testRetrieveTafNotFound() throws IOException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(404);
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
ParseException e = assertThrows(ParseException.class, () -> sut.retrieveTaf("lftm"));
assertEquals(ErrorCodes.ERROR_CODE_INVALID_ICAO, e.getErrorCode());
}
@Test
void testRetrieveTaf() throws ParseException, IOException, URISyntaxException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn(Stream.of(
+ "TAF LFPG 121700Z 1218/1324 13003KT CAVOK TX09/1315Z TN00/1306Z",
+ "TEMPO 1303/1308 4000 BR"));
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
String result = sut.retrieveTaf("LFPG");
+
assertNotNull(result);
assertTrue(result.contains("LFPG"));
}
diff --git a/metarParser-services/src/test/java/io/github/mivek/service/provider/NOAAWeatherProviderTest.java b/metarParser-services/src/test/java/io/github/mivek/service/provider/NOAAWeatherProviderTest.java
index 69cfcc77..95b8f2b6 100644
--- a/metarParser-services/src/test/java/io/github/mivek/service/provider/NOAAWeatherProviderTest.java
+++ b/metarParser-services/src/test/java/io/github/mivek/service/provider/NOAAWeatherProviderTest.java
@@ -2,19 +2,43 @@
import io.github.mivek.exception.ErrorCodes;
import io.github.mivek.exception.ParseException;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
class NOAAWeatherProviderTest {
- private final NOAAWeatherProvider sut = new NOAAWeatherProvider();
+ private HttpClient mockHttpClient;
+ private HttpResponse> mockResponse = mock(HttpResponse.class);
+ private NOAAWeatherProvider sut;
+
+ @BeforeEach
+ void setUp() {
+ mockHttpClient = mock(HttpClient.class);
+ mockResponse = mock(HttpResponse.class);
+ sut = new NOAAWeatherProvider(mockHttpClient);
+ }
+
+ @Test
+ void testConstructors() {
+ assertNotNull(new NOAAWeatherProvider());
+ assertNotNull(new NOAAWeatherProvider("Custom-Agent"));
+ }
@Test
void testRetrieveMetarInvalidIcao() {
@@ -23,14 +47,24 @@ void testRetrieveMetarInvalidIcao() {
}
@Test
- void testRetrieveMetarNotFound() {
+ void testRetrieveMetarNotFound() throws IOException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(404);
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
ParseException e = assertThrows(ParseException.class, () -> sut.retrieveMetar("lftm"));
assertEquals(ErrorCodes.ERROR_CODE_INVALID_ICAO, e.getErrorCode());
}
@Test
void testRetrieveMetar() throws ParseException, IOException, URISyntaxException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn(Stream.of(
+ "2024/01/01 18:30",
+ "LFPG 251830Z 17013KT 9999 OVC006 04/03 Q1012 NOSIG"));
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
String result = sut.retrieveMetar("LFPG");
+
assertNotNull(result);
assertTrue(result.contains("LFPG"));
}
@@ -42,14 +76,25 @@ void testRetrieveTafInvalidIcao() {
}
@Test
- void testRetrieveTafNotFound() {
+ void testRetrieveTafNotFound() throws IOException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(404);
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
ParseException e = assertThrows(ParseException.class, () -> sut.retrieveTaf("lftm"));
assertEquals(ErrorCodes.ERROR_CODE_INVALID_ICAO, e.getErrorCode());
}
@Test
void testRetrieveTaf() throws ParseException, IOException, URISyntaxException, InterruptedException {
+ when(mockResponse.statusCode()).thenReturn(200);
+ when(mockResponse.body()).thenReturn(Stream.of(
+ "2024/01/01 18:30",
+ "TAF LFPG 121700Z 1218/1324 13003KT CAVOK TX09/1315Z TN00/1306Z",
+ "TEMPO 1303/1308 4000 BR"));
+ doReturn(mockResponse).when(mockHttpClient).send(any(HttpRequest.class), any());
+
String result = sut.retrieveTaf("LFPG");
+
assertNotNull(result);
assertTrue(result.contains("LFPG"));
}
@@ -117,4 +162,9 @@ void testFormat() throws ParseException {
assertNotNull(result);
assertEquals(formatted, result);
}
+
+ @Test
+ void testDefaultUserAgent() {
+ assertEquals("MetarParser", AbstractWeatherProvider.DEFAULT_USER_AGENT);
+ }
}
diff --git a/pom.xml b/pom.xml
index 61b61450..2dfd82d4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,7 @@
1.14.1
1.2.3
1.23.0
+ 5.17.0
2.0.17
4.9.8.2
mivek-github
@@ -115,6 +116,12 @@
${archunit-junit5.version}
test
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
org.slf4j
slf4j-nop