Skip to content

Commit

Permalink
Add Quarkus CLI TLS command test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Sep 5, 2024
1 parent d82523f commit a0b6119
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 1 deletion.
10 changes: 9 additions & 1 deletion quarkus-cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
<artifactId>quarkus-test-cli</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-tls-registry</artifactId>
<!-- provided so that we can use API in unit test and compilation succeeds -->
<!-- it is only in generated app where Quarkus TLS registry extension is present -->
<scope>provided</scope>
</dependency>
</dependencies>
<profiles>
<profile>
Expand All @@ -41,7 +48,8 @@
<!-- This will allow us to have unit tests for apps created via CLI in src/test/java -->
<!-- Which makes them better maintainable from IDE than Java classes in resources -->
<!-- Also this way, compilation fails if some bump breaks them -->
<!-- See the 'io.quarkus.ts.quarkus.cli.config.surefire' package for respective unit tests -->
<!-- See the 'io.quarkus.ts.quarkus.cli.config.surefire' package -->
<!-- and the 'io.quarkus.ts.quarkus.cli.tls.surefire' package for respective unit tests -->
<configuration>
<skipTests>true</skipTests>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.quarkus.ts.quarkus.cli;

import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.CERT_NAME;
import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.CN;
import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.PASSWORD;
import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.TRUST_STORE_PATH;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.util.function.Function;

import jakarta.inject.Inject;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import io.quarkus.logging.Log;
import io.quarkus.test.bootstrap.QuarkusCliCommandResult;
import io.quarkus.test.bootstrap.tls.GenerateCertOptions;
import io.quarkus.test.bootstrap.tls.GenerateQuarkusCaOptions;
import io.quarkus.test.bootstrap.tls.QuarkusTlsCommand;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.scenarios.annotations.DisabledOnNative;
import io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest;

@Tag("QUARKUS-4592")
@Tag("quarkus-cli")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@QuarkusScenario
@DisabledOnNative // Only for JVM verification
public class QuarkusCliTlsCommandIT {

// locations are expected according to the Quarkus docs describes generation target
private static final File QUARKUS_BASE_DIR = new File(System.getenv("HOME"), ".quarkus");
private static final File DEV_CA_CERT_FILE = new File(QUARKUS_BASE_DIR, "quarkus-dev-root-ca.pem");
private static final File DEV_CA_PK_FILE = new File(QUARKUS_BASE_DIR, "quarkus-dev-root-key.pem");

@Inject
static QuarkusTlsCommand tlsCommand;

@Order(1)
@Test
public void generateQuarkusCa() {
// also prepares state for assertion in TlsCommandTest
deleteFileIfExists(DEV_CA_CERT_FILE);
deleteFileIfExists(DEV_CA_PK_FILE);
tlsCommand
.generateQuarkusCa()
.withOption(GenerateQuarkusCaOptions.TRUSTSTORE_LONG)
.withOption(GenerateQuarkusCaOptions.RENEW_SHORT)
.executeCommand()
.assertCommandOutputContains("Root CA certificate generated successfully")
.assertCommandOutputContains("Quarkus Development CA generated and installed")
.assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Truststore generated successfully:"));
assertTrue(DEV_CA_CERT_FILE.exists(),
"Quarkus CLI subcommand 'tls generate-quarkus-ca' didn't generate Quarkus DEV CA certificate");
assertTrue(DEV_CA_PK_FILE.exists(),
"Quarkus CLI subcommand 'tls generate-quarkus-ca' didn't generate Quarkus DEV CA private key");
}

@Order(2)
@Test
public void generateCertificate() {
// also prepares state for assertion in TlsCommandTest
String appSvcDir = tlsCommand.getApp().getServiceFolder().toAbsolutePath().toString();
tlsCommand
.generateCertificate()
.withOption(GenerateCertOptions.COMMON_NAME_LONG, CN)
.withOption(GenerateCertOptions.NAME_SHORT, CERT_NAME)
.withOption(GenerateCertOptions.PASSWORD_SHORT, PASSWORD)
.withOption(GenerateCertOptions.DIRECTORY_LONG, appSvcDir)
.executeCommand()
.assertCommandOutputContains("Quarkus Dev CA certificate found at " + DEV_CA_CERT_FILE.getAbsolutePath())
.assertCommandOutputContains("PKCS12 keystore and truststore generated successfully!")
.assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Key Store File:"))
.assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Trust Store File:"))
// save truststore path in application properties so that we can use it in TlsCommandTest
.addToAppProps(cmd -> TRUST_STORE_PATH + "=" + cmd.getOutputLineRemainder("Trust Store File:"))
.assertCommandOutputContains(
"Signed Certificate generated successfully and exported into `%s-keystore.p12`".formatted(CERT_NAME))
// following properties are set by this tls command, and we want to use them TlsCommandTest as well
.addToAppProps(getPropertyFromEnvFileAndChangeProfileToTest("quarkus.tls.key-store.p12.path"))
.addToAppProps(getPropertyFromEnvFileAndChangeProfileToTest("quarkus.tls.key-store.p12.password"));
}

@Order(3)
@Test
public void runTestsUsingGeneratedCerts() {
tlsCommand.buildAppAndExpectSuccess(TlsCommandTest.class);
}

private static void deleteFileIfExists(File file) {
if (file.exists()) {
// better inform so that user know his local Quarkus DEV CA is gone
Log.info("Deleting file: " + file);
if (!file.delete()) {
throw new IllegalStateException("Failed to delete file: " + file);
}
}
}

@Test
public void testHelpOption() {
tlsCommand.generateQuarkusCa()
.withOption(GenerateQuarkusCaOptions.HELP_LONG)
.executeCommand()
.assertCommandOutputContains("--install")
.assertCommandOutputContains("--renew")
.assertCommandOutputContains("--truststore")
.assertCommandOutputContains("Generate Quarkus Dev CA certificate and private key")
.assertCommandOutputContains("Install the generated CA into the system keychain")
.assertCommandOutputContains("Update certificate if already created")
.assertCommandOutputContains("Generate a PKCS12");
tlsCommand.generateCertificate()
.withOption(GenerateCertOptions.HELP_SHORT)
.executeCommand()
.assertCommandOutputContains("--directory")
.assertCommandOutputContains("--name")
.assertCommandOutputContains("--password")
.assertCommandOutputContains("--renew")
.assertCommandOutputContains("Generate a TLS certificate with the Quarkus Dev CA if available")
.assertCommandOutputContains("The common name of the certificate")
.assertCommandOutputContains("The directory in which the certificates will be created")
.assertCommandOutputContains("Name of the certificate")
.assertCommandOutputContains("The password of the keystore")
.assertCommandOutputContains("Whether existing certificates will need to be replaced");
}

private static Function<QuarkusCliCommandResult, String> getPropertyFromEnvFileAndChangeProfileToTest(String propertyKey) {
return cmd -> {
// add generated env vars also to application properties under test profile
// so that we can also use them in TlsCommandTest
var propertyValue = cmd.getPropertyValueFromEnvFile("%dev." + propertyKey);
return "%test." + propertyKey + "=" + propertyValue;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.quarkus.ts.quarkus.cli.tls.surefire;

import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.net.URL;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.HashSet;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.restassured.RestAssured;
import io.smallrye.config.SmallRyeConfig;

/**
* This test is only support to run inside QuarkusCliTlsCommandIT.
*/
@QuarkusTest
public class TlsCommandTest {

public static final String CN = "quarkus-qe-cn";
public static final String CERT_NAME = "quarkus-qe-cert";
public static final String PASSWORD = "quarkus-qe-password";
public static final String TRUST_STORE_PATH = "quarkus-qe-test.trust-store-path";

@Inject
TlsConfigurationRegistry registry;

@Inject
SmallRyeConfig config;

@TestHTTPResource(value = "/hello", tls = true)
URL helloEndpointUrl;

@Test
void testKeystoreInDefaultTlsRegistry() throws KeyStoreException {
var defaultRegistry = registry.getDefault()
.orElseThrow(() -> new AssertionError("Default TLS Registry is not configured"));
var ks = defaultRegistry.getKeyStore();
var ksAliasesSet = new HashSet<String>();
var ksAliases = ks.aliases();
while (ksAliases.hasMoreElements()) {
ksAliasesSet.add(ksAliases.nextElement());
}
assertTrue(ksAliasesSet.contains(CERT_NAME));
assertTrue(ksAliasesSet.contains("issuer-" + CERT_NAME));

try {
var key = ks.getKey(CERT_NAME, PASSWORD.toCharArray());
assertNotNull(key);
// this is not set in the stone so we don't mind if it changes to something sensible
// the point here is that we get Key and work with it ...
assertEquals("RSA", key.getAlgorithm());
} catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {
throw new RuntimeException(e);
}
}

@Test
void testCommunicationUsingGeneratedCerts() {
try {
// failure test: make sure that generated truststore is really required
// so that we know that the truststore generated with Quarkus CLI TLS command works
RestAssured
.given()
.get(helloEndpointUrl).then().statusCode(200);
Assertions.fail("Truststore is not required, therefore we cannot verify generated truststore");
} catch (Exception ignored) {
// failure expected
}

var truststorePath = config.getValue(TRUST_STORE_PATH, String.class);
RestAssured
.given()
.trustStore(new File(truststorePath), PASSWORD)
.get(helloEndpointUrl).then().statusCode(200).body(is("Hello from Quarkus REST"));
}

}

0 comments on commit a0b6119

Please sign in to comment.