diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml
index 31b632fc..2355e17a 100644
--- a/config/checkstyle/checkstyle-suppressions.xml
+++ b/config/checkstyle/checkstyle-suppressions.xml
@@ -15,6 +15,7 @@
+
diff --git a/src/main/java/dev/sorn/fmp4j/cfg/FmpConfig.java b/src/main/java/dev/sorn/fmp4j/cfg/FmpConfig.java
index 21ab6bcb..2ca32408 100644
--- a/src/main/java/dev/sorn/fmp4j/cfg/FmpConfig.java
+++ b/src/main/java/dev/sorn/fmp4j/cfg/FmpConfig.java
@@ -1,7 +1,9 @@
package dev.sorn.fmp4j.cfg;
+import dev.sorn.fmp4j.types.FmpApiKey;
+
public interface FmpConfig {
- String fmpApiKey();
+ FmpApiKey fmpApiKey();
String fmpBaseUrl();
}
diff --git a/src/main/java/dev/sorn/fmp4j/cfg/FmpConfigImpl.java b/src/main/java/dev/sorn/fmp4j/cfg/FmpConfigImpl.java
index e1393e3c..5f3704ff 100644
--- a/src/main/java/dev/sorn/fmp4j/cfg/FmpConfigImpl.java
+++ b/src/main/java/dev/sorn/fmp4j/cfg/FmpConfigImpl.java
@@ -3,15 +3,17 @@
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
+import dev.sorn.fmp4j.types.FmpApiKey;
+
public final class FmpConfigImpl implements FmpConfig {
public static final String FMP4J_API_KEY_ENV = "FMP_API_KEY";
public static final String FMP4J_BASE_URL_ENV = "FMP_BASE_URL";
- private final String fmpApiKey;
+ private final FmpApiKey fmpApiKey;
private final String fmpBaseUrl;
public FmpConfigImpl() {
- fmpApiKey = getRequiredEnv(FMP4J_API_KEY_ENV);
+ fmpApiKey = new FmpApiKey(getRequiredEnv(FMP4J_API_KEY_ENV));
fmpBaseUrl = getRequiredEnv(FMP4J_BASE_URL_ENV);
}
@@ -27,7 +29,7 @@ private String getRequiredEnv(String name) {
}
@Override
- public String fmpApiKey() {
+ public FmpApiKey fmpApiKey() {
return fmpApiKey;
}
diff --git a/src/main/java/dev/sorn/fmp4j/exceptions/FmpInvalidApiKey.java b/src/main/java/dev/sorn/fmp4j/exceptions/FmpInvalidApiKey.java
new file mode 100644
index 00000000..141443ea
--- /dev/null
+++ b/src/main/java/dev/sorn/fmp4j/exceptions/FmpInvalidApiKey.java
@@ -0,0 +1,7 @@
+package dev.sorn.fmp4j.exceptions;
+
+public class FmpInvalidApiKey extends FmpException {
+ public FmpInvalidApiKey(String message, Object... args) {
+ super(message, args);
+ }
+}
diff --git a/src/main/java/dev/sorn/fmp4j/types/FmpApiKey.java b/src/main/java/dev/sorn/fmp4j/types/FmpApiKey.java
new file mode 100644
index 00000000..851f1726
--- /dev/null
+++ b/src/main/java/dev/sorn/fmp4j/types/FmpApiKey.java
@@ -0,0 +1,52 @@
+package dev.sorn.fmp4j.types;
+
+import static java.lang.String.format;
+
+import dev.sorn.fmp4j.exceptions.FmpInvalidApiKey;
+import java.util.regex.Pattern;
+
+public class FmpApiKey implements FmpValueObject {
+ public static final Pattern FMP_API_KEY_PATTERN = Pattern.compile("^[A-Za-z\\d]{32}$");
+
+ private final String value;
+
+ public FmpApiKey(String value) {
+ if (value == null || value.isBlank()) {
+ throw new FmpInvalidApiKey("'value' must not be null or blank");
+ }
+ if (!FMP_API_KEY_PATTERN.matcher(value).matches()) {
+ throw new FmpInvalidApiKey(
+ format("'value' [%s] does not match pattern [%s]", value, FMP_API_KEY_PATTERN.pattern()));
+ }
+ this.value = value;
+ }
+
+ @Override
+ public String value() {
+ return value.substring(0, 2) + "*".repeat(value.length() - 4) + value.substring(value.length() - 2);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof FmpApiKey that)) {
+ return false;
+ }
+ return value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return value();
+ }
+}
diff --git a/src/test/java/dev/sorn/fmp4j/FmpClientTest.java b/src/test/java/dev/sorn/fmp4j/FmpClientTest.java
index 1127c674..4f969a28 100644
--- a/src/test/java/dev/sorn/fmp4j/FmpClientTest.java
+++ b/src/test/java/dev/sorn/fmp4j/FmpClientTest.java
@@ -70,6 +70,7 @@
import dev.sorn.fmp4j.models.FmpStock;
import dev.sorn.fmp4j.models.FmpStockPriceChange;
import dev.sorn.fmp4j.models.FmpTreasuryRate;
+import dev.sorn.fmp4j.types.FmpApiKey;
import dev.sorn.fmp4j.types.FmpSymbol;
import java.net.URI;
import java.time.LocalDate;
@@ -86,7 +87,7 @@
class FmpClientTest {
private static final String BASE_URL = "https://financialmodelingprep.com/stable";
- private static final String API_KEY = "424242";
+ private static final FmpApiKey API_KEY = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
private final FmpConfig fmpConfig = mock(FmpConfig.class);
private final FmpHttpClient fmpHttpClient = mock(FmpHttpClient.class);
private FmpClient fmpClient;
diff --git a/src/test/java/dev/sorn/fmp4j/cfg/FmpConfigImplTest.java b/src/test/java/dev/sorn/fmp4j/cfg/FmpConfigImplTest.java
index b6d8f47e..956ff926 100644
--- a/src/test/java/dev/sorn/fmp4j/cfg/FmpConfigImplTest.java
+++ b/src/test/java/dev/sorn/fmp4j/cfg/FmpConfigImplTest.java
@@ -26,7 +26,7 @@ void load_fmp_api_key_from_env() {
// then
assertNotNull(fmpApiKey);
- assertEquals("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6", fmpApiKey);
+ assertEquals("AB****************************y6", fmpApiKey.value());
}
@Test
diff --git a/src/test/java/dev/sorn/fmp4j/clients/FmpChartClientTest.java b/src/test/java/dev/sorn/fmp4j/clients/FmpChartClientTest.java
index 6f734f2d..d8702961 100644
--- a/src/test/java/dev/sorn/fmp4j/clients/FmpChartClientTest.java
+++ b/src/test/java/dev/sorn/fmp4j/clients/FmpChartClientTest.java
@@ -5,6 +5,7 @@
import dev.sorn.fmp4j.cfg.FmpConfig;
import dev.sorn.fmp4j.http.FmpHttpClient;
+import dev.sorn.fmp4j.types.FmpApiKey;
import org.junit.jupiter.api.BeforeEach;
class FmpChartClientTest {
@@ -14,7 +15,7 @@ class FmpChartClientTest {
@BeforeEach
void setUp() {
- when(cfg.fmpApiKey()).thenReturn("abc123");
+ when(cfg.fmpApiKey()).thenReturn(new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6"));
client = new FmpChartClient(cfg, http);
}
}
diff --git a/src/test/java/dev/sorn/fmp4j/types/FmpApiKeyTest.java b/src/test/java/dev/sorn/fmp4j/types/FmpApiKeyTest.java
new file mode 100644
index 00000000..1496f448
--- /dev/null
+++ b/src/test/java/dev/sorn/fmp4j/types/FmpApiKeyTest.java
@@ -0,0 +1,149 @@
+package dev.sorn.fmp4j.types;
+
+import static dev.sorn.fmp4j.types.FmpApiKey.FMP_API_KEY_PATTERN;
+import static java.lang.String.format;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import dev.sorn.fmp4j.exceptions.FmpInvalidApiKey;
+import java.util.function.Function;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class FmpApiKeyTest {
+ @Test
+ void sensitive_data_is_masked() {
+ // given
+ var apiKey = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+
+ // when
+ var val = apiKey.value();
+ var str = apiKey.toString();
+
+ // then
+ assertEquals("AB****************************y6", val);
+ assertEquals("AB****************************y6", str);
+ }
+
+ @Test
+ void null_key_throws() {
+ // given
+ var nullKey = (String) null;
+
+ // when
+ Function f = FmpApiKey::new;
+
+ // then
+ var e = assertThrows(FmpInvalidApiKey.class, () -> f.apply(nullKey));
+ assertEquals("'value' must not be null or blank", e.getMessage());
+ }
+
+ @Test
+ void blank_key_throws() {
+ // given
+ var blankKey = "";
+
+ // when
+ Function f = FmpApiKey::new;
+
+ // then
+ var e = assertThrows(FmpInvalidApiKey.class, () -> f.apply(blankKey));
+ assertEquals("'value' must not be null or blank", e.getMessage());
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "ABCDEf0ghIjklmNO1pqRsT2u34VWx5y", // too short
+ "ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6z", // too long
+ "ABCDEf0ghIjklmN$1pqRsT2u34VWx5y6" // invalid symbol $
+ })
+ void invalid_pattern_throws(String value) {
+ // when
+ Function f = FmpApiKey::new;
+
+ // then
+ var e = assertThrows(FmpInvalidApiKey.class, () -> f.apply(value));
+ assertEquals(format("'value' [%s] does not match pattern [%s]", value, FMP_API_KEY_PATTERN), e.getMessage());
+ }
+
+ @Test
+ void hashCode_value() {
+ // given
+ var str = "ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6";
+ var k = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+
+ // when
+ var hc = k.hashCode();
+
+ // then
+ assertEquals(str.hashCode(), hc);
+ }
+
+ @Test
+ void equals_same_true() {
+ // given
+ var k = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+
+ // when
+ var eq = k.equals(k);
+
+ // then
+ assertTrue(eq);
+ }
+
+ @Test
+ void equals_identical_true() {
+ // given
+ var k1 = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+ var k2 = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+
+ // when
+ var eq = k1.equals(k2);
+
+ // then
+ assertTrue(eq);
+ }
+
+ @Test
+ void equals_null_false() {
+ // given
+ var k1 = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+ var k2 = (FmpApiKey) null;
+
+ // when
+ var eq = k1.equals(k2);
+
+ // then
+ assertFalse(eq);
+ }
+
+ @Test
+ void equals_different_false() {
+ // given
+ var k1 = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+ var k2 = new FmpApiKey("ABcDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+
+ // when
+ var eq = k1.equals(k2);
+
+ // then
+ assertFalse(eq);
+ }
+
+ @Test
+ void equals_wrong_instance_false() {
+ // given
+ var k1 = new FmpApiKey("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6");
+ var k2 = "ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6";
+
+ // when
+ var eq = k1.equals(k2);
+
+ // then
+ assertFalse(eq);
+ }
+}