Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<suppress files=".*[\\/]generated-sources[\\/].*" checks=".*"/>

<!-- Alphabetically sorted -->
<suppress files="FmpApiKey.java" checks="MagicNumber"/> <!-- value() -->
<suppress files="FmpClient.java" checks="ParameterNumber"/> <!-- constructor() -->
<suppress files="FmpHttpClientImpl.java" checks="AvoidInlineConditionals"/> <!-- getJsonList() -->
<suppress files="FmpIncomeStatementGrowth.java" checks="AbbreviationAsWordInName"/> <!-- growthEBITDA -->
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/dev/sorn/fmp4j/cfg/FmpConfig.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package dev.sorn.fmp4j.cfg;

import dev.sorn.fmp4j.types.FmpApiKey;

public interface FmpConfig {
String fmpApiKey();
FmpApiKey fmpApiKey();

String fmpBaseUrl();
}
8 changes: 5 additions & 3 deletions src/main/java/dev/sorn/fmp4j/cfg/FmpConfigImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -27,7 +29,7 @@ private String getRequiredEnv(String name) {
}

@Override
public String fmpApiKey() {
public FmpApiKey fmpApiKey() {
return fmpApiKey;
}

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/dev/sorn/fmp4j/exceptions/FmpInvalidApiKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.sorn.fmp4j.exceptions;

public class FmpInvalidApiKey extends FmpException {
public FmpInvalidApiKey(String message, Object... args) {
super(message, args);
}
}
52 changes: 52 additions & 0 deletions src/main/java/dev/sorn/fmp4j/types/FmpApiKey.java
Original file line number Diff line number Diff line change
@@ -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<String> {
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();
}
}
3 changes: 2 additions & 1 deletion src/test/java/dev/sorn/fmp4j/FmpClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/dev/sorn/fmp4j/cfg/FmpConfigImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ void load_fmp_api_key_from_env() {

// then
assertNotNull(fmpApiKey);
assertEquals("ABCDEf0ghIjklmNO1pqRsT2u34VWx5y6", fmpApiKey);
assertEquals("AB****************************y6", fmpApiKey.value());
}

@Test
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/dev/sorn/fmp4j/clients/FmpChartClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}
149 changes: 149 additions & 0 deletions src/test/java/dev/sorn/fmp4j/types/FmpApiKeyTest.java
Original file line number Diff line number Diff line change
@@ -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<String, FmpApiKey> 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<String, FmpApiKey> 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<String, FmpApiKey> 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);
}
}
Loading