Good progress — down to 4 failures from the original pile. Let me trace each one precisely.
Failure 1 & 2 — RSAAdapterResponseTest empty XML tests:
parseAndUnwrap_emptyXml_returnsEmptyResponseShape:125
expected: not <null>
parseAndUnwrap_emptyXml_analyze_returnsAnalyzeShape:132
expected: not <null>
The empty response handler returns { notifyResponse: { notifyReturn: {} } } — but our test asserts result.get("notifyReturn") directly. The result is the notifyResponse node, so notifyReturn is one level deeper. Test assertion is wrong, not the production code.
Error 3 & 4 — RSACryptoUtilTest:
encryptThenDecrypt_roundTripSucceeds:98 IllegalArgument: Illegal base64 character 2d
encrypt_reuseKeyBranch:123 IllegalArgument: Illegal base64 character 2d
Character 2d is a hyphen -. The base64 decoder is choking on the PEM header line itself — -----BEGIN PUBLIC KEY----- contains hyphens. This means our stripped string still has header content. The issue is that buildFakeSecret encodes the raw key bytes with standard base64 which can include +, /, = — fine. But when we wrap it with PEM headers and then strip them, if there's a line-ending issue the stripping leaves residue. Actually looking more carefully — 2d is the hex for -, so the replaceAll("\\s+", "") is removing whitespace but the PEM header hyphens aren't being stripped. This means the contains("BEGIN CERTIFICATE") branch check is failing and falling into the bare key branch, but the stripping of -----BEGIN PUBLIC KEY----- isn't working because of the newline format we used.
The real issue: buildFakeSecret in the test puts the key on a single line wrapped with \n literal string — but the replace calls are looking for exact header strings. If there's any whitespace variation it fails.
All four fixes — make these changes:
Fix 1 — RSAAdapterResponseTest empty XML assertions. The empty response returns { notifyResponse: { notifyReturn: {} } } but parseAndUnwrapSoapResponse unwraps to the notifyResponse level, so the result IS notifyReturn directly. Wait — re-reading parseAndUnwrapSoapResponse: the empty path does:
ret.set(normalizedType + "Return", objectMapper.createObjectNode());
empty.set(normalizedType + "Response", ret);
return empty;
So it returns empty which is { notifyResponse: { notifyReturn: {} } }. The test asserts result.get("notifyReturn") — but notifyReturn is nested under notifyResponse. Fix the assertions:
@Test
void parseAndUnwrap_emptyXml_returnsEmptyResponseShape() throws Exception {
JsonNode result = parseAndUnwrap("", "notify");
assertNotNull(result);
// empty path returns { notifyResponse: { notifyReturn: {} } }
assertNotNull(result.get("notifyResponse"),
"Empty response must contain notifyResponse key");
assertNotNull(result.get("notifyResponse").get("notifyReturn"),
"Empty response must contain notifyReturn inside notifyResponse");
}
@Test
void parseAndUnwrap_emptyXml_analyze_returnsAnalyzeShape() throws Exception {
JsonNode result = parseAndUnwrap("", "analyze");
assertNotNull(result.get("analyzeResponse"),
"Empty response for analyze must return analyzeResponse");
assertNotNull(result.get("analyzeResponse").get("analyzeReturn"),
"Empty response must contain analyzeReturn inside analyzeResponse");
}
Fix 2 — RSACryptoUtilTest.buildFakeSecret. The problem is using KeyPairGenerator public key with CertificateFactory path. Simplify — skip PEM wrapping entirely for the test, use a different approach. Replace buildFakeSecret with a version that uses PKCS#8 private key (what KeyPairGenerator natively produces) and bare public key, but make the header strings unambiguous:
private ObjectNode buildFakeSecret(KeyPair kp) throws Exception {
ObjectNode secret = mapper.createObjectNode();
secret.put("currentVersion", "1");
// Bare public key — triggers the X509EncodedKeySpec branch
// Use explicit line breaks to ensure header stripping works
String pubB64 = Base64.getMimeEncoder(64, new byte[]{'\n'})
.encodeToString(kp.getPublic().getEncoded());
secret.put("APICPayloadEncryptionKey-1",
"-----BEGIN PUBLIC KEY-----\n"
+ pubB64
+ "\n-----END PUBLIC KEY-----");
// PKCS#8 private key — triggers the PKCS#8 branch in parsePkcs1PrivateKeyFromPem
// Note: KeyPairGenerator produces PKCS#8, not PKCS#1.
// We test PKCS#1 parsing in a dedicated test below.
// Here we need the decrypt path to work, so use PKCS#8 headers.
String privB64 = Base64.getMimeEncoder(64, new byte[]{'\n'})
.encodeToString(kp.getPrivate().getEncoded());
secret.put("APICPayloadDecryptionKey-1",
"-----BEGIN PRIVATE KEY-----\n"
+ privB64
+ "\n-----END PRIVATE KEY-----");
return secret;
}
And update parsePkcs1PrivateKeyFromPem in RSACryptoUtil.java to also handle -----BEGIN PRIVATE KEY----- (PKCS#8 header) so the test works — rename the method to handle both:
private static PrivateKey parsePrivateKeyFromPem(String pem) throws Exception {
String normalized = pem.replace("\\n", "\n").trim();
if (normalized.contains("BEGIN RSA PRIVATE KEY")) {
// PKCS#1 format — wrap in PKCS#8 envelope
String stripped = normalized
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] pkcs1Der = Base64.getDecoder().decode(stripped);
// ... same PKCS#8 wrapping code as before ...
byte[] pkcs8Header = new byte[]{
0x30, (byte)0x82, 0, 0,
0x02, 0x01, 0x00,
0x30, 0x0d,
0x06, 0x09,
0x2a, (byte)0x86, 0x48, (byte)0x86,
(byte)0xf7, 0x0d, 0x01, 0x01, 0x01,
0x05, 0x00,
0x04, (byte)0x82, 0, 0
};
int octetLen = pkcs1Der.length;
int totalLen = pkcs8Header.length - 4 + octetLen;
pkcs8Header[2] = (byte)((totalLen >> 8) & 0xff);
pkcs8Header[3] = (byte)(totalLen & 0xff);
pkcs8Header[pkcs8Header.length - 2] = (byte)((octetLen >> 8) & 0xff);
pkcs8Header[pkcs8Header.length - 1] = (byte)(octetLen & 0xff);
byte[] pkcs8Der = new byte[pkcs8Header.length + pkcs1Der.length];
System.arraycopy(pkcs8Header, 0, pkcs8Der, 0, pkcs8Header.length);
System.arraycopy(pkcs1Der, 0, pkcs8Der, pkcs8Header.length, pkcs1Der.length);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(new PKCS8EncodedKeySpec(pkcs8Der));
} else {
// PKCS#8 format ("BEGIN PRIVATE KEY") — JDK handles natively
String stripped = normalized
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] der = Base64.getDecoder().decode(stripped);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(new PKCS8EncodedKeySpec(der));
}
}
Then update the two call sites in decrypt and encrypt from parsePkcs8PrivateKeyFromPem / parsePkcs1PrivateKeyFromPem to parsePrivateKeyFromPem. Delete the old method entirely.
Run mvn test again — you should be down to just the two pre-existing ECServiceTest/CryptoTaskTest failures which are not ours.

























