Skip to content

Commit 6d3ab01

Browse files
edridudidudiedriclaude
authored
Test the utils and factory packages (#213)
Both were the thinnest packages in the codebase - utils at 57.8% line coverage and factory at 56.2% - and HexUtil had no tests at all. HexUtil 11 tests: round trips, 0x prefix handling, odd-length and non-hex rejection, negative bytes not sign-extending, and a round trip across all 256 byte values Bech32Util 7 tests: the accepting side, which nothing covered. Real mainnet stake addresses, pool ids and drep ids validate, and a corrupted checksum that still passes the charset check is rejected Tuple 3 tests BackendFactory 8 tests: every overload resolves to the expected base URL, all ten services are wired, and the utility class cannot be instantiated Coverage on the same measurement basis: utils 57.8% -> 92.2% factory 56.2% -> 90.6% overall 76.9% -> 81.5% lines, 63.4% -> 70.6% branches All 29 run offline in under a second, so they add nothing to CI time and cannot flake on a live Koios instance. Co-authored-by: dudiedri <dudi.edri@ox.security> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent ea5bc39 commit 6d3ab01

4 files changed

Lines changed: 275 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package rest.koios.client.backend.factory;
2+
3+
import org.junit.jupiter.api.Test;
4+
import rest.koios.client.backend.api.base.BaseService;
5+
6+
import java.lang.reflect.Constructor;
7+
import java.lang.reflect.InvocationTargetException;
8+
import java.lang.reflect.Modifier;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
class BackendFactoryTest {
13+
14+
private static String baseUrlOf(BackendService service) {
15+
return ((BaseService) service.getNetworkService()).getRetrofit().baseUrl().toString();
16+
}
17+
18+
@Test
19+
void mainnetPointsAtMainnetTest() {
20+
assertEquals("https://api.koios.rest/api/v1/", baseUrlOf(BackendFactory.getKoiosMainnetService()));
21+
assertEquals("https://api.koios.rest/api/v1/", baseUrlOf(BackendFactory.getKoiosMainnetService("token")));
22+
assertEquals("https://api.koios.rest/api/v1/",
23+
baseUrlOf(BackendFactory.getKoiosMainnetService(ApiVersion.VERSION_1, "token")));
24+
}
25+
26+
@Test
27+
void eachNetworkGetsItsOwnHostTest() {
28+
assertEquals("https://guild.koios.rest/api/v1/", baseUrlOf(BackendFactory.getKoiosGuildService()));
29+
assertEquals("https://preview.koios.rest/api/v1/", baseUrlOf(BackendFactory.getKoiosPreviewService()));
30+
assertEquals("https://preprod.koios.rest/api/v1/", baseUrlOf(BackendFactory.getKoiosPreprodService()));
31+
}
32+
33+
@Test
34+
void tokenOverloadsResolveToTheSameHostTest() {
35+
assertEquals(baseUrlOf(BackendFactory.getKoiosGuildService()),
36+
baseUrlOf(BackendFactory.getKoiosGuildService("token")));
37+
assertEquals(baseUrlOf(BackendFactory.getKoiosPreviewService()),
38+
baseUrlOf(BackendFactory.getKoiosPreviewService(ApiVersion.VERSION_1, "token")));
39+
assertEquals(baseUrlOf(BackendFactory.getKoiosPreprodService()),
40+
baseUrlOf(BackendFactory.getKoiosPreprodService("token")));
41+
}
42+
43+
@Test
44+
void customRpcUsesTheGivenUrlTest() {
45+
String url = "http://localhost:8080/api/v1/";
46+
assertEquals(url, baseUrlOf(BackendFactory.getCustomRPCService(url)));
47+
assertEquals(url, baseUrlOf(BackendFactory.getCustomRPCService(url, "token")));
48+
}
49+
50+
@Test
51+
void everyServiceIsWiredTest() {
52+
BackendService service = BackendFactory.getKoiosMainnetService();
53+
assertNotNull(service.getNetworkService());
54+
assertNotNull(service.getEpochService());
55+
assertNotNull(service.getBlockService());
56+
assertNotNull(service.getTransactionsService());
57+
assertNotNull(service.getAccountService());
58+
assertNotNull(service.getAddressService());
59+
assertNotNull(service.getAssetService());
60+
assertNotNull(service.getPoolService());
61+
assertNotNull(service.getScriptService());
62+
assertNotNull(service.getGovernanceService());
63+
}
64+
65+
@Test
66+
void apiVersionCarriesItsPathSegmentTest() {
67+
assertEquals("v1", ApiVersion.VERSION_1.getVersion());
68+
assertEquals(1, ApiVersion.values().length);
69+
}
70+
71+
@Test
72+
void operationTypesCarryTheirUrlsTest() {
73+
assertEquals("https://api.koios.rest/api/", OperationType.KOIOS_MAINNET.getBaseUrl());
74+
assertEquals("https://guild.koios.rest/api/", OperationType.KOIOS_GUILD.getBaseUrl());
75+
assertEquals("https://preview.koios.rest/api/", OperationType.KOIOS_PREVIEW.getBaseUrl());
76+
assertEquals("https://preprod.koios.rest/api/", OperationType.KOIOS_PREPROD.getBaseUrl());
77+
}
78+
79+
@Test
80+
void factoryIsNotInstantiableTest() throws NoSuchMethodException {
81+
Constructor<BackendFactory> constructor = BackendFactory.class.getDeclaredConstructor();
82+
assertTrue(Modifier.isPrivate(constructor.getModifiers()));
83+
constructor.setAccessible(true);
84+
InvocationTargetException thrown =
85+
assertThrows(InvocationTargetException.class, constructor::newInstance);
86+
assertInstanceOf(IllegalStateException.class, thrown.getCause());
87+
}
88+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package rest.koios.client.utils;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
/**
8+
* Covers the accepting side of {@link Bech32Util}, which the existing tests do not reach -
9+
* they only assert that malformed input is rejected.
10+
*/
11+
class Bech32UtilValidationTest {
12+
13+
// real mainnet identifiers
14+
private static final String STAKE_ADDRESS =
15+
"stake1uyrx65wjqjgeeksd8hptmcgl5jfyrqkfq0xe8xlp367kphsckq250";
16+
private static final String POOL_ID =
17+
"pool1z5uqdk7dzdxaae5633fqfcu2eqzy3a3rgtuvy087fdld7yws0xt";
18+
private static final String DREP_ID =
19+
"drep1ygqzg3ed7rdqeg3343jw0fptqzc3lqtk3rvnnmgq64rj85sxd4sr4";
20+
21+
@Test
22+
void acceptsRealIdentifiersTest() {
23+
assertTrue(Bech32Util.isValid(STAKE_ADDRESS));
24+
assertTrue(Bech32Util.isValid(POOL_ID));
25+
assertTrue(Bech32Util.isValid(DREP_ID));
26+
}
27+
28+
@Test
29+
void acceptsValidCharactersTest() {
30+
assertTrue(Bech32Util.hasValidChars(STAKE_ADDRESS));
31+
assertTrue(Bech32Util.hasValidChars(POOL_ID));
32+
}
33+
34+
@Test
35+
void rejectsCorruptedChecksumTest() {
36+
// flip the final character: still valid bech32 charset, wrong checksum
37+
char last = POOL_ID.charAt(POOL_ID.length() - 1);
38+
char swapped = last == 't' ? 'v' : 't';
39+
String corrupted = POOL_ID.substring(0, POOL_ID.length() - 1) + swapped;
40+
41+
assertTrue(Bech32Util.hasValidChars(corrupted), "charset is still valid");
42+
assertFalse(Bech32Util.isValid(corrupted), "checksum must fail");
43+
}
44+
45+
@Test
46+
void rejectsMixedCaseTest() {
47+
String mixed = "Stake1uyrx65wjqjgeeksd8hptmcgl5jfyrqkfq0xe8xlp367kphsckq250";
48+
assertFalse(Bech32Util.hasValidChars(mixed));
49+
assertFalse(Bech32Util.isValid(mixed));
50+
}
51+
52+
@Test
53+
void rejectsMissingSeparatorTest() {
54+
assertFalse(Bech32Util.hasValidChars("thisstringhasnoseparatoratall"));
55+
}
56+
57+
@Test
58+
void rejectsTooShortForChecksumTest() {
59+
// has a separator and valid chars, but no room for a checksum
60+
assertFalse(Bech32Util.isValid("a1qqq"));
61+
}
62+
63+
@Test
64+
void rejectsNullAndEmptyTest() {
65+
assertFalse(Bech32Util.isValid(null));
66+
assertFalse(Bech32Util.isValid(""));
67+
}
68+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package rest.koios.client.utils;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
class HexUtilTest {
8+
9+
private static final String TX_HASH =
10+
"4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6";
11+
12+
@Test
13+
void encodeDecodeRoundTripsTest() {
14+
byte[] bytes = HexUtil.decodeHexString(TX_HASH);
15+
assertEquals(32, bytes.length);
16+
assertEquals(TX_HASH, HexUtil.encodeHexString(bytes));
17+
}
18+
19+
@Test
20+
void encodeWithPrefixTest() {
21+
byte[] bytes = {(byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef};
22+
assertEquals("deadbeef", HexUtil.encodeHexString(bytes));
23+
assertEquals("deadbeef", HexUtil.encodeHexString(bytes, false));
24+
assertEquals("0xdeadbeef", HexUtil.encodeHexString(bytes, true));
25+
}
26+
27+
@Test
28+
void decodeAcceptsPrefixTest() {
29+
assertArrayEquals(HexUtil.decodeHexString("deadbeef"),
30+
HexUtil.decodeHexString("0xdeadbeef"));
31+
}
32+
33+
@Test
34+
void encodeNullReturnsNullTest() {
35+
assertNull(HexUtil.encodeHexString(null));
36+
assertNull(HexUtil.encodeHexString(null, true));
37+
}
38+
39+
@Test
40+
void encodeEmptyArrayTest() {
41+
assertEquals("", HexUtil.encodeHexString(new byte[0]));
42+
assertEquals("0x", HexUtil.encodeHexString(new byte[0], true));
43+
}
44+
45+
@Test
46+
void decodeRejectsOddLengthTest() {
47+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
48+
() -> HexUtil.decodeHexString("abc"));
49+
assertTrue(e.getMessage().contains("Invalid hexadecimal String"), e.getMessage());
50+
}
51+
52+
@Test
53+
void decodeRejectsNullTest() {
54+
assertThrows(IllegalArgumentException.class, () -> HexUtil.decodeHexString(null));
55+
}
56+
57+
@Test
58+
void decodeRejectsNonHexCharacterTest() {
59+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
60+
() -> HexUtil.decodeHexString("zz"));
61+
assertTrue(e.getMessage().contains("Invalid Hexadecimal Character"), e.getMessage());
62+
}
63+
64+
@Test
65+
void byteToHexCoversSignedRangeTest() {
66+
assertEquals("00", HexUtil.byteToHex((byte) 0));
67+
assertEquals("0f", HexUtil.byteToHex((byte) 15));
68+
assertEquals("7f", HexUtil.byteToHex(Byte.MAX_VALUE));
69+
// negative bytes must not sign-extend
70+
assertEquals("80", HexUtil.byteToHex(Byte.MIN_VALUE));
71+
assertEquals("ff", HexUtil.byteToHex((byte) -1));
72+
}
73+
74+
@Test
75+
void hexToByteTest() {
76+
assertEquals((byte) 0, HexUtil.hexToByte("00"));
77+
assertEquals((byte) 255, HexUtil.hexToByte("ff"));
78+
assertEquals((byte) 255, HexUtil.hexToByte("FF"));
79+
assertEquals((byte) 171, HexUtil.hexToByte("ab"));
80+
}
81+
82+
@Test
83+
void allByteValuesRoundTripTest() {
84+
byte[] all = new byte[256];
85+
for (int i = 0; i < 256; i++) {
86+
all[i] = (byte) i;
87+
}
88+
assertArrayEquals(all, HexUtil.decodeHexString(HexUtil.encodeHexString(all)));
89+
}
90+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package rest.koios.client.utils;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
class TupleTest {
8+
9+
@Test
10+
void holdsBothValuesTest() {
11+
Tuple<String, Integer> tuple = new Tuple<>("policy", 42);
12+
assertEquals("policy", tuple._1);
13+
assertEquals(42, tuple._2);
14+
}
15+
16+
@Test
17+
void acceptsNullsTest() {
18+
Tuple<String, String> tuple = new Tuple<>(null, null);
19+
assertNull(tuple._1);
20+
assertNull(tuple._2);
21+
}
22+
23+
@Test
24+
void supportsMixedTypesTest() {
25+
Tuple<byte[], Boolean> tuple = new Tuple<>(new byte[]{1, 2}, true);
26+
assertArrayEquals(new byte[]{1, 2}, tuple._1);
27+
assertTrue(tuple._2);
28+
}
29+
}

0 commit comments

Comments
 (0)