Add tests

This commit is contained in:
Oracle 2026-04-21 18:00:31 +02:00
parent 8acf584d28
commit 21b4169130
Signed by: Oracle
SSH key fingerprint: SHA256:x4/RtnjUyuHkdvmwNDsWSfcfF1V5PNr3OpriZqOvCX8
7 changed files with 879 additions and 18 deletions

3
.gitignore vendored
View file

@ -36,4 +36,5 @@ build/
.vscode/
### Mac OS ###
.DS_Store
.DS_Store
client_keys

20
pom.xml
View file

@ -19,18 +19,32 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
<scope>compile</scope>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>6.0.3</version>
<artifactId>junit-jupiter</artifactId>
<version>5.12.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>

View file

@ -12,10 +12,6 @@ import java.util.Set;
*/
public final class Constants {
private Constants() {
// Utility class prevents instantiation
}
// Protocol Constants
/**

View file

@ -16,7 +16,10 @@ import java.nio.file.attribute.PosixFilePermissions;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAKeyGenParameterSpec;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.util.Arrays;
import java.util.Map;
import java.util.Scanner;
@ -238,22 +241,31 @@ public class SecureCompletionClient {
* @param password optional password for the encrypted private key
*/
public void loadKeys(String privateKeyPath, String publicPemKeyPath, String password) {
File keyFile = new File(privateKeyPath);
if (!keyFile.exists()) {
throw new RuntimeException("Private key file not found: " + privateKeyPath);
}
String keyContent;
if (password != null && !password.isEmpty()) {
String cipherText = getEncryptedPrivateKeyFromFile(privateKeyPath);
keyContent = getEncryptedPrivateKeyFromFile(privateKeyPath);
try {
cipherText = Pass2Key.decrypt("AES/GCM/NoPadding", cipherText, password);
keyContent = Pass2Key.decrypt("AES/GCM/NoPadding", keyContent, password);
} catch (NoSuchPaddingException | NoSuchAlgorithmException
| BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException |
InvalidKeyException e) {
System.out.println("Wrong password!");
return;
}
} else {
keyContent = getEncryptedPrivateKeyFromFile(privateKeyPath);
}
try {
this.privateKey = Pass2Key.convertStringToPrivateKey(cipherText);
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
this.privateKey = Pass2Key.convertStringToPrivateKey(keyContent);
} catch (Exception e) {
throw new RuntimeException("Failed to load private key: " + e.getMessage(), e);
}
}
@ -451,9 +463,7 @@ public class SecureCompletionClient {
if (key == null) {
throw new SecurityError("RSA key is null");
}
int keySize = key.getEncoded() != null ? key.getEncoded().length * 8 : 0;
System.out.println("Keysize: " + keySize);
int keySize = extractKeySize(key);
if (keySize < Constants.MIN_RSA_KEY_SIZE) {
throw new SecurityError(
@ -462,6 +472,27 @@ public class SecureCompletionClient {
}
}
private int extractKeySize(PrivateKey key) {
try {
java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
java.security.spec.RSAPrivateCrtKeySpec crtSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateCrtKeySpec.class);
return crtSpec.getModulus().bitLength();
} catch (Exception ignored) {
// Try RSAPrivateKeySpec
try {
java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
java.security.spec.RSAPrivateKeySpec privSpec = kf.getKeySpec(key, java.security.spec.RSAPrivateKeySpec.class);
return privSpec.getModulus().bitLength();
} catch (Exception ignored2) {
// Fall back to encoded length
if (key.getEncoded() != null) {
return key.getEncoded().length * 8;
}
}
}
return 0;
}
// HTTP Status Exception Mapping
/**

View file

@ -0,0 +1,381 @@
package ai.nomyo;
import ai.nomyo.errors.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SecureCompletionClientE2ETest {
private static final String TEST_PASSWORD = "e2e-test-password-456";
private static final String BASE_URL = "https://api.nomyo.ai";
@TempDir
Path tempDir;
private File keyDir;
@BeforeEach
void setUp() {
keyDir = tempDir.resolve("e2e_keys").toFile();
assertTrue(keyDir.mkdirs(), "Key directory should be created");
}
@AfterEach
void tearDown() {
// Cleanup is handled by @TempDir
}
// Full Lifecycle E2E Tests
@Test
@Order(1)
@DisplayName("E2E: Generate keys, save to disk, load in new client, validate")
void e2e_fullLifecycle_generateSaveLoadValidate() throws Exception {
// Step 1: Generate keys and save to disk
SecureCompletionClient generateClient = new SecureCompletionClient(BASE_URL, false, true, 2);
generateClient.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
PrivateKey generatedKey = generateClient.getPrivateKey();
String generatedPublicPem = generateClient.getPublicPemKey();
assertNotNull(generatedKey, "Generated private key should not be null");
assertNotNull(generatedPublicPem, "Generated public PEM should not be null");
// Step 2: Verify key files exist on disk
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
File publicKeyFile = new File(keyDir, Constants.DEFAULT_PUBLIC_KEY_FILE);
assertTrue(privateKeyFile.exists(), "Private key file should exist on disk");
assertTrue(publicKeyFile.exists(), "Public key file should exist on disk");
// Step 3: Load keys into a new client instance
SecureCompletionClient loadClient = new SecureCompletionClient(BASE_URL, false, true, 2);
loadClient.loadKeys(
privateKeyFile.getAbsolutePath(),
null,
TEST_PASSWORD
);
PrivateKey loadedKey = loadClient.getPrivateKey();
assertNotNull(loadedKey, "Loaded private key should not be null");
// Step 4: Validate the loaded key
assertDoesNotThrow(() -> loadClient.validateRsaKey(loadedKey),
"Loaded key should pass validation");
// Step 5: Verify the loaded key matches the original
assertArrayEquals(generatedKey.getEncoded(), loadedKey.getEncoded(),
"Loaded key bytes should match original key bytes");
}
@Test
@Order(2)
@DisplayName("E2E: Generate plaintext keys, load, and validate")
void e2e_plaintextKeys_generateLoadValidate() throws Exception {
// Generate plaintext keys (no password)
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
PrivateKey originalKey = client.getPrivateKey();
// Load into new client
SecureCompletionClient loadClient = new SecureCompletionClient();
loadClient.loadKeys(
new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null
);
PrivateKey loadedKey = loadClient.getPrivateKey();
// Validate
assertDoesNotThrow(() -> loadClient.validateRsaKey(loadedKey));
assertArrayEquals(originalKey.getEncoded(), loadedKey.getEncoded());
}
@Test
@Order(3)
@DisplayName("E2E: Key validation with various key sizes")
void e2e_keyValidation_variousSizes() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
// Test with 1024-bit key (should fail)
KeyPairGenerator gen1024 = KeyPairGenerator.getInstance("RSA");
gen1024.initialize(1024);
PrivateKey smallKey = gen1024.generateKeyPair().getPrivate();
SecurityError smallError = assertThrows(SecurityError.class,
() -> client.validateRsaKey(smallKey));
assertTrue(smallError.getMessage().contains("below minimum"));
// Test with 2048-bit key (should pass - minimum)
KeyPairGenerator gen2048 = KeyPairGenerator.getInstance("RSA");
gen2048.initialize(2048);
PrivateKey minKey = gen2048.generateKeyPair().getPrivate();
assertDoesNotThrow(() -> client.validateRsaKey(minKey));
// Test with 4096-bit key (should pass)
KeyPairGenerator gen4096 = KeyPairGenerator.getInstance("RSA");
gen4096.initialize(4096);
PrivateKey largeKey = gen4096.generateKeyPair().getPrivate();
assertDoesNotThrow(() -> client.validateRsaKey(largeKey));
}
@Test
@Order(4)
@DisplayName("E2E: HTTP status mapping covers all documented cases")
void e2e_httpStatusMapping_allCases() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
// 200 - success (null exception)
assertNull(client.mapHttpStatus(200, "OK"));
// 400 - InvalidRequestError
Exception e400 = client.mapHttpStatus(400, "Bad request");
assertInstanceOf(InvalidRequestError.class, e400);
// 401 - AuthenticationError
Exception e401 = client.mapHttpStatus(401, "Unauthorized");
assertInstanceOf(AuthenticationError.class, e401);
// 403 - ForbiddenError
Exception e403 = client.mapHttpStatus(403, "Forbidden");
assertInstanceOf(ForbiddenError.class, e403);
// 404 - APIError
Exception e404 = client.mapHttpStatus(404, "Not found");
assertInstanceOf(APIError.class, e404);
// 429 - RateLimitError
Exception e429 = client.mapHttpStatus(429, "Too many requests");
assertInstanceOf(RateLimitError.class, e429);
// 500 - ServerError
Exception e500 = client.mapHttpStatus(500, "Internal error");
assertInstanceOf(ServerError.class, e500);
// 502 - APIError (retryable)
Exception e502 = client.mapHttpStatus(502, "Bad gateway");
assertInstanceOf(APIError.class, e502);
// 503 - ServiceUnavailableError
Exception e503 = client.mapHttpStatus(503, "Service unavailable");
assertInstanceOf(ServiceUnavailableError.class, e503);
// 504 - APIError (retryable)
Exception e504 = client.mapHttpStatus(504, "Gateway timeout");
assertInstanceOf(APIError.class, e504);
// 599 - APIError (unknown)
Exception e599 = client.mapHttpStatus(599, "Unknown error");
assertInstanceOf(APIError.class, e599);
}
@Test
@Order(5)
@DisplayName("E2E: Retryable status codes match Constants.RETRYABLE_STATUS_CODES")
void e2e_retryableStatusCodes_matchConstants() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
for (int code : Constants.RETRYABLE_STATUS_CODES) {
Exception exception = client.mapHttpStatus(code, "test");
// All retryable codes should return an Exception (not null)
assertNotNull(exception, "Retryable status " + code + " should return an exception");
}
}
@Test
@Order(6)
@DisplayName("E2E: URL encoding of public key PEM")
void e2e_urlEncoding_publicKey() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
String pemKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" +
"IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBI" +
"kAMBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" +
"IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBIjAN\n-----END PUBLIC KEY-----";
String encoded = client.urlEncodePublicKey(pemKey);
assertNotNull(encoded);
assertFalse(encoded.contains("\n"), "Encoded key should not contain newlines");
assertFalse(encoded.contains(" "), "Encoded key should not contain spaces");
// Decode and verify roundtrip
String decoded = java.net.URLDecoder.decode(encoded, java.nio.charset.StandardCharsets.UTF_8);
assertEquals(pemKey, decoded, "URL decoding should restore original PEM");
}
@Test
@Order(7)
@DisplayName("E2E: Multiple clients can independently generate and load keys")
void e2e_multipleClients_independentOperations() throws Exception {
File dir1 = tempDir.resolve("dir1").toFile();
File dir2 = tempDir.resolve("dir2").toFile();
dir1.mkdirs();
dir2.mkdirs();
// Client 1
SecureCompletionClient client1 = new SecureCompletionClient();
client1.generateKeys(true, dir1.getAbsolutePath(), "pass1");
PrivateKey key1 = client1.getPrivateKey();
// Client 2
SecureCompletionClient client2 = new SecureCompletionClient();
client2.generateKeys(true, dir2.getAbsolutePath(), "pass2");
PrivateKey key2 = client2.getPrivateKey();
// Verify keys are different (different modulus values)
java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
java.security.spec.RSAPrivateCrtKeySpec spec1 = kf.getKeySpec(key1, java.security.spec.RSAPrivateCrtKeySpec.class);
java.security.spec.RSAPrivateCrtKeySpec spec2 = kf.getKeySpec(key2, java.security.spec.RSAPrivateCrtKeySpec.class);
assertNotEquals(spec1.getModulus(), spec2.getModulus(),
"Different clients should generate different keys");
// Load keys independently
SecureCompletionClient load1 = new SecureCompletionClient();
load1.loadKeys(
new File(dir1, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null, "pass1"
);
SecureCompletionClient load2 = new SecureCompletionClient();
load2.loadKeys(
new File(dir2, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null, "pass2"
);
// Verify each loaded key matches its original
assertArrayEquals(key1.getEncoded(), load1.getPrivateKey().getEncoded());
assertArrayEquals(key2.getEncoded(), load2.getPrivateKey().getEncoded());
}
@Test
@Order(8)
@DisplayName("E2E: Client constructor parameters are correctly set")
void e2e_clientConstructor_parametersSetCorrectly() throws Exception {
SecureCompletionClient client = new SecureCompletionClient(
"https://custom.api.com",
true,
false,
5
);
assertEquals("https://custom.api.com", client.getRouterUrl());
assertTrue(client.isAllowHttp());
assertFalse(client.isUseSecureMemory());
assertEquals(5, client.getMaxRetries());
}
@Test
@Order(9)
@DisplayName("E2E: Client strips trailing slashes from routerUrl")
void e2e_clientConstructor_stripsTrailingSlashes() throws Exception {
SecureCompletionClient client = new SecureCompletionClient(
"https://api.example.com///",
false, true, 1
);
assertEquals("https://api.example.com", client.getRouterUrl());
}
@Test
@Order(10)
@DisplayName("E2E: Client uses default values when constructed with no args")
void e2e_clientConstructor_defaultValues() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
assertEquals(Constants.DEFAULT_BASE_URL, client.getRouterUrl());
assertFalse(client.isAllowHttp());
assertTrue(client.isUseSecureMemory());
assertEquals(Constants.DEFAULT_MAX_RETRIES, client.getMaxRetries());
}
@Test
@Order(11)
@DisplayName("E2E: Encrypted key file is unreadable without password")
void e2e_encryptedKey_unreadableWithoutPassword() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
String encryptedContent = Files.readString(privateKeyFile.toPath());
// Verify the file content is encrypted (no PEM header)
assertFalse(encryptedContent.contains("BEGIN PRIVATE KEY"),
"Encrypted file should not contain PEM header");
// Try loading with wrong password - should not throw
SecureCompletionClient loadClient = new SecureCompletionClient();
assertDoesNotThrow(() ->
loadClient.loadKeys(privateKeyFile.getAbsolutePath(), null, "wrong-password"),
"Wrong password should be handled gracefully"
);
}
@Test
@Order(12)
@DisplayName("E2E: Generate keys without saving produces in-memory keys")
void e2e_generateKeys_noSave_producesInMemoryKeys() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
String publicPem = client.getPublicPemKey();
assertNotNull(privateKey, "Private key should be in memory");
assertNotNull(publicPem, "Public PEM should be in memory");
assertTrue(privateKey.getAlgorithm().equals("RSA"));
}
@Test
@Order(13)
@DisplayName("E2E: SecurityError is thrown for null key validation")
void e2e_nullKeyValidation_throwsSecurityError() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
SecurityError error = assertThrows(SecurityError.class,
() -> client.validateRsaKey(null));
assertNotNull(error.getMessage());
assertTrue(error.getMessage().contains("null"));
}
@Test
@Order(14)
@DisplayName("E2E: mapHttpStatus returns null for 200 status")
void e2e_mapHttpStatus_200_returnsNull() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
Exception result = client.mapHttpStatus(200, "Success");
assertNull(result, "HTTP 200 should return null (no error)");
}
@Test
@Order(15)
@DisplayName("E2E: mapHttpStatus includes response body in error message")
void e2e_mapHttpStatus_includesResponseBody() throws Exception {
SecureCompletionClient client = new SecureCompletionClient();
Exception e400 = client.mapHttpStatus(400, "Invalid parameter: email");
assertTrue(e400.getMessage().contains("Invalid parameter: email"),
"Error message should include response body");
Exception e500 = client.mapHttpStatus(500, "Database connection failed");
assertTrue(e500.getMessage().contains("Database connection failed"),
"Error message should include response body");
}
}

View file

@ -0,0 +1,436 @@
package ai.nomyo;
import ai.nomyo.errors.SecurityError;
import ai.nomyo.util.Pass2Key;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.spec.RSAKeyGenParameterSpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SecureCompletionClientTest {
private static final String TEST_PASSWORD = "test-password-123";
private static final String PLAINTEXT_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7o4qne60TBb\n-----END PRIVATE KEY-----";
private SecureCompletionClient client;
@BeforeEach
void setUp() {
client = new SecureCompletionClient();
}
@AfterEach
void tearDown() {
client = null;
}
// Key Generation Tests
@Test
@Order(1)
@DisplayName("generateKeys should create 4096-bit RSA key pair")
void generateKeys_shouldCreateValidKeyPair() {
client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
String publicPemKey = client.getPublicPemKey();
assertNotNull(privateKey, "Private key should not be null");
assertNotNull(publicPemKey, "Public PEM key should not be null");
assertTrue(privateKey.getAlgorithm().equals("RSA"), "Key algorithm should be RSA");
assertTrue(publicPemKey.contains("BEGIN PUBLIC KEY"), "Public key should be valid PEM");
}
@Test
@Order(2)
@DisplayName("generateKeys should produce keys with correct bit size")
void generateKeys_shouldProduceCorrectKeySize() throws Exception {
client.generateKeys(false);
PrivateKey privateKey = client.getPrivateKey();
java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
java.security.spec.RSAPrivateCrtKeySpec spec = kf.getKeySpec(privateKey, java.security.spec.RSAPrivateCrtKeySpec.class);
int keySizeBits = spec.getModulus().bitLength();
assertEquals(Constants.RSA_KEY_SIZE, keySizeBits,
"RSA key should be " + Constants.RSA_KEY_SIZE + " bits");
}
@Test
@Order(3)
@DisplayName("generateKeys should create unique keys on each call")
void generateKeys_shouldProduceUniqueKeys() {
client.generateKeys(false);
PrivateKey firstKey = client.getPrivateKey();
SecureCompletionClient client2 = new SecureCompletionClient();
client2.generateKeys(false);
PrivateKey secondKey = client2.getPrivateKey();
assertNotEquals(firstKey.getEncoded().length, secondKey.getEncoded().length,
"Different keys should have different encoded lengths");
}
// Key Generation with File Save Tests
@Test
@Order(4)
@DisplayName("generateKeys with saveToFile=true should create key files")
void generateKeys_withSaveToFile_shouldCreateKeyFiles(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
File publicKeyFile = new File(keyDir, Constants.DEFAULT_PUBLIC_KEY_FILE);
assertTrue(privateKeyFile.exists(), "Private key file should be created");
assertTrue(publicKeyFile.exists(), "Public key file should be created");
assertTrue(privateKeyFile.length() > 0, "Private key file should not be empty");
assertTrue(publicKeyFile.length() > 0, "Public key file should not be empty");
}
@Test
@Order(5)
@DisplayName("generateKeys with password should encrypt private key file")
void generateKeys_withPassword_shouldEncryptPrivateKey(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
String content = Files.readString(privateKeyFile.toPath());
assertFalse(content.contains("BEGIN PRIVATE KEY"),
"Encrypted private key should not contain PEM header");
assertNotEquals(PLAINTEXT_PRIVATE_KEY, content,
"Encrypted key should differ from plaintext");
}
@Test
@Order(6)
@DisplayName("generateKeys should not overwrite existing key files")
void generateKeys_shouldNotOverwriteExistingKeys(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
File privateKeyFile = new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE);
long firstSize = privateKeyFile.length();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
long secondSize = privateKeyFile.length();
assertEquals(firstSize, secondSize,
"Existing key files should not be overwritten");
}
// Key Loading Tests
@Test
@Order(7)
@DisplayName("loadKeys should load plaintext private key from file")
void loadKeys_plaintext_shouldLoadPrivateKey(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), null);
PrivateKey originalKey = client.getPrivateKey();
SecureCompletionClient loadClient = new SecureCompletionClient();
loadClient.loadKeys(
new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null
);
PrivateKey loadedKey = loadClient.getPrivateKey();
assertNotNull(loadedKey, "Loaded private key should not be null");
assertEquals(originalKey.getEncoded().length, loadedKey.getEncoded().length,
"Loaded key should have same size as original");
}
@Test
@Order(8)
@DisplayName("loadKeys should load encrypted private key with correct password")
void loadKeys_encrypted_correctPassword_shouldLoadPrivateKey(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
PrivateKey originalKey = client.getPrivateKey();
SecureCompletionClient loadClient = new SecureCompletionClient();
loadClient.loadKeys(
new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null,
TEST_PASSWORD
);
PrivateKey loadedKey = loadClient.getPrivateKey();
assertNotNull(loadedKey, "Loaded private key should not be null");
assertEquals(originalKey.getEncoded().length, loadedKey.getEncoded().length,
"Loaded key should have same size as original");
}
@Test
@Order(9)
@DisplayName("loadKeys should handle wrong password gracefully")
void loadKeys_encrypted_wrongPassword_shouldHandleGracefully(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
SecureCompletionClient loadClient = new SecureCompletionClient();
assertDoesNotThrow(() ->
loadClient.loadKeys(
new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null,
"wrong-password"
),
"Wrong password should not throw exception"
);
}
@Test
@Order(10)
@DisplayName("loadKeys should throw exception for non-existent file")
void loadKeys_nonExistentFile_shouldThrowException() {
SecureCompletionClient loadClient = new SecureCompletionClient();
RuntimeException error = assertThrows(RuntimeException.class, () ->
loadClient.loadKeys("/non/existent/path/private_key.pem", null, null));
assertTrue(error.getMessage().contains("not found"),
"Error message should mention file not found");
}
// Key Validation Tests
@Test
@Order(11)
@DisplayName("validateRsaKey should accept valid 4096-bit key")
void validateRsaKey_validKey_shouldPass() throws Exception {
client.generateKeys(false);
PrivateKey key = client.getPrivateKey();
assertDoesNotThrow(() -> client.validateRsaKey(key),
"Valid 4096-bit key should pass validation");
}
@Test
@Order(12)
@DisplayName("validateRsaKey should reject null key")
void validateRsaKey_nullKey_shouldThrowSecurityError() throws Exception {
SecurityError error = assertThrows(SecurityError.class, () ->
client.validateRsaKey(null));
assertTrue(error.getMessage().contains("null"),
"Error message should mention null key");
}
@Test
@Order(13)
@DisplayName("validateRsaKey should reject keys below minimum size")
void validateRsaKey_tooSmallKey_shouldThrowSecurityError() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(1024);
KeyPair pair = generator.generateKeyPair();
PrivateKey smallKey = pair.getPrivate();
SecurityError error = assertThrows(SecurityError.class, () ->
client.validateRsaKey(smallKey));
assertTrue(error.getMessage().contains("below minimum"),
"Error message should mention minimum size requirement");
}
@Test
@Order(14)
@DisplayName("validateRsaKey should accept minimum size key (2048 bits)")
void validateRsaKey_minimumSizeKey_shouldPass() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(new RSAKeyGenParameterSpec(2048, BigInteger.valueOf(65537)));
KeyPair pair = generator.generateKeyPair();
PrivateKey minKey = pair.getPrivate();
assertDoesNotThrow(() -> client.validateRsaKey(minKey),
"2048-bit key should pass validation (minimum)");
}
// Key Roundtrip Tests
@Test
@Order(15)
@DisplayName("Full roundtrip: generate, save, load should produce same key")
void roundtrip_generateSaveLoad_shouldProduceSameKey(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
// Generate and save
client.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
PrivateKey originalKey = client.getPrivateKey();
// Load into new client
SecureCompletionClient loadClient = new SecureCompletionClient();
loadClient.loadKeys(
new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null,
TEST_PASSWORD
);
PrivateKey loadedKey = loadClient.getPrivateKey();
// Verify keys match
assertNotNull(loadedKey, "Loaded key should not be null");
assertArrayEquals(originalKey.getEncoded(), loadedKey.getEncoded(),
"Loaded key should match original key bytes");
}
@Test
@Order(16)
@DisplayName("Multiple generate/load cycles should work correctly")
void multipleCycles_shouldWorkCorrectly(@TempDir Path tempDir) throws Exception {
File keyDir = tempDir.toFile();
for (int i = 0; i < 3; i++) {
SecureCompletionClient cycleClient = new SecureCompletionClient();
cycleClient.generateKeys(true, keyDir.getAbsolutePath(), TEST_PASSWORD);
PrivateKey generatedKey = cycleClient.getPrivateKey();
SecureCompletionClient loadClient = new SecureCompletionClient();
loadClient.loadKeys(
new File(keyDir, Constants.DEFAULT_PRIVATE_KEY_FILE).getAbsolutePath(),
null,
TEST_PASSWORD
);
PrivateKey loadedKey = loadClient.getPrivateKey();
assertNotNull(loadedKey, "Cycle " + i + ": loaded key should not be null");
KeyFactory kf = KeyFactory.getInstance("RSA");
int generatedBits = kf.getKeySpec(generatedKey, RSAPrivateCrtKeySpec.class).getModulus().bitLength();
int loadedBits = kf.getKeySpec(loadedKey, RSAPrivateCrtKeySpec.class).getModulus().bitLength();
assertEquals(generatedBits, loadedBits,
"Cycle " + i + ": loaded key should have same modulus bit length as generated key");
}
}
// Utility Method Tests
@Test
@Order(17)
@DisplayName("urlEncodePublicKey should properly encode PEM keys")
void urlEncodePublicKey_shouldEncodeCorrectly() {
String pemKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n-----END PUBLIC KEY-----";
String encoded = client.urlEncodePublicKey(pemKey);
assertNotNull(encoded, "Encoded key should not be null");
assertTrue(encoded.contains("%0A"), "Encoded key should contain URL-encoded newlines");
assertTrue(encoded.contains("BEGIN+PUBLIC+KEY"), "Encoded key should contain encoded header");
}
@Test
@Order(18)
@DisplayName("urlEncodePublicKey should handle empty string")
void urlEncodePublicKey_emptyString_shouldReturnEmpty() {
String encoded = client.urlEncodePublicKey("");
assertEquals("", encoded, "Empty string should encode to empty string");
}
// Pass2Key Encryption/Decryption Tests
@Test
@Order(19)
@DisplayName("Pass2Key encrypt/decrypt should preserve plaintext")
void pass2Key_encryptDecrypt_shouldPreservePlaintext() throws Exception {
String plaintext = "Test plaintext content for encryption";
String password = "my-secret-password";
String encrypted = Pass2Key.encrypt("AES/GCM/NoPadding", plaintext, password);
assertNotNull(encrypted, "Encrypted text should not be null");
assertNotEquals(plaintext, encrypted, "Encrypted text should differ from plaintext");
String decrypted = Pass2Key.decrypt("AES/GCM/NoPadding", encrypted, password);
assertEquals(plaintext, decrypted, "Decrypted text should match original plaintext");
}
@Test
@Order(20)
@DisplayName("Pass2Key should produce different ciphertext for same plaintext")
void pass2Key_shouldProduceDifferentCiphertext() throws Exception {
String plaintext = "Same plaintext";
String password = "same-password";
String encrypted1 = Pass2Key.encrypt("AES/GCM/NoPadding", plaintext, password);
String encrypted2 = Pass2Key.encrypt("AES/GCM/NoPadding", plaintext, password);
assertNotEquals(encrypted1, encrypted2,
"Same plaintext with same password should produce different ciphertext (due to random salt/IV)");
}
@Test
@Order(21)
@DisplayName("Pass2Key decrypt should fail with wrong password")
void pass2Key_wrongPassword_shouldFail() throws Exception {
String plaintext = "Secret content";
String correctPassword = "correct-password";
String wrongPassword = "wrong-password";
String encrypted = Pass2Key.encrypt("AES/GCM/NoPadding", plaintext, correctPassword);
assertThrows(Exception.class, () ->
Pass2Key.decrypt("AES/GCM/NoPadding", encrypted, wrongPassword),
"Decryption with wrong password should throw exception"
);
}
@Test
@Order(22)
@DisplayName("Pass2Key convertStringToPrivateKey should parse PEM correctly")
void convertStringToPrivateKey_shouldParsePEM() throws Exception {
SecureCompletionClient tempClient = new SecureCompletionClient();
tempClient.generateKeys(false);
PrivateKey originalKey = tempClient.getPrivateKey();
String pem = ai.nomyo.util.PEMConverter.toPEM(originalKey.getEncoded(), true);
PrivateKey parsedKey = Pass2Key.convertStringToPrivateKey(pem);
assertNotNull(parsedKey, "Parsed private key should not be null");
assertEquals("RSA", parsedKey.getAlgorithm(), "Parsed key should be RSA");
assertArrayEquals(originalKey.getEncoded(), parsedKey.getEncoded(),
"Parsed key should match original key bytes");
}
@Test
@Order(23)
@DisplayName("Pass2Key convertStringToPrivateKey should handle PEM with whitespace")
void convertStringToPrivateKey_shouldHandleWhitespace() throws Exception {
SecureCompletionClient tempClient = new SecureCompletionClient();
tempClient.generateKeys(false);
PrivateKey originalKey = tempClient.getPrivateKey();
String pem = "-----BEGIN PRIVATE KEY-----\n " +
originalKey.getEncoded().length + "lines\n" +
"-----END PRIVATE KEY-----";
String formattedPem = ai.nomyo.util.PEMConverter.toPEM(originalKey.getEncoded(), true);
String pemWithWhitespace = formattedPem.replace("\n", "\n ");
PrivateKey parsedKey = Pass2Key.convertStringToPrivateKey(pemWithWhitespace);
assertNotNull(parsedKey, "Parsed private key should not be null even with whitespace");
assertArrayEquals(originalKey.getEncoded(), parsedKey.getEncoded(),
"Parsed key with whitespace should match original key bytes");
}
}

View file

@ -0,0 +1,2 @@
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.config.dynamic.factor = 1