【问题标题】:Mixed up Test configuration when using @ResourceArg使用@ResourceArg 时混淆了测试配置
【发布时间】:2023-01-09 19:46:24
【问题描述】:

长话短说:博士;当使用不同的 @ResourceArgs 运行测试时,不同测试的配置会被丢弃并覆盖其他测试,从而破坏本应使用特定配置运行的测试。

因此,我有一项服务,该服务具有在不同配置设置中运行的测试。目前的主要区别是该服务可以管理自己的身份验证或从外部来源(Keycloak)获取身份验证。

我首先使用测试配置文件来控制它,这似乎工作正常。不幸的是,为了支持这两种情况,我设置的 ResourceLifecycleManager 支持设置 Keycloak 实例并返回破坏自我身份验证配置的配置值(这主要是因为我还没有找到如何获取生命周期管理器自行确定当前正在运行的配置文件或配置。如果我能做到这一点,我想我会比使用 @ResourceArg 好得多,所以很想知道我是否在这里遗漏了什么)。

为了弥补这个缺点,我尝试使用@ResourceArgs 来向生命周期管理器传达何时设置外部身份验证。但是,我注意到一些非常奇怪的执行时间,并且根据测试类的注释,在我的测试/服务中结束的配置不是我想要的,很明显生命周期管理器已经设置了外部身份验证。

此外,应该注意的是,我已经订购了我的测试,这样配置文件和配置就不会乱序运行;首先运行所有不关心的测试,然后是使用自我身份验证的“正常”测试,然后是使用外部身份验证配置文件的测试。当我在 intellij 中运行时,我可以看到它正常工作,而且我可以看出在测试配置文件之间启动新服务实例所花费的时间。

当我在某些地方设置断点时查看日志,一些奇怪的事情是显而易见的:

  • 在错误测试上设置断点时(在外部配置的测试运行之前)
    • 我的TestResourceLifecycleManager的start()方法被调用了两次
      • 第一次运行时使用 Keycloak 启动,会覆盖/破坏配置
        • 虽然我预计启动 keycloak 所需的时间没有发生,但这里有点困惑
      • 第二次运行正确,没有启动keycloak
    • 配置文件配置是预期的,除了 keycloak 设置将覆盖的内容
  • 在外部配置的测试上设置断点时(在所有自配置测试运行之后):
    • start()方法现在已经被调用了4次;对于应用程序的新运行,事情似乎再次以与以前相同的顺序开始

Intellij/Gradle 显示日志的方式可能有些奇怪,但我将其解释为:

  • Quarkus 出于某种原因在启动应用程序时启动了 LifecycleManager 的两个实例,一个的配置覆盖了另一个,导致我的麻烦。
  • 生命周期管理器按预期工作;无论以哪种方式配置,它都会适当地启动/不启动keycloak

在这一点上,我不知道是我做错了什么,还是有错误。

自授权测试的测试类示例(此 (test) 配置文件中所有测试的相同注释):

@Slf4j
@QuarkusTest
@QuarkusTestResource(TestResourceLifecycleManager.class)
@TestHTTPEndpoint(Auth.class)
class AuthTest extends RunningServerTest {

外部身份验证测试的测试类示例(此 (externalAuth) 配置文件中所有测试的相同注释):

@Slf4j
@QuarkusTest
@TestProfile(ExternalAuthTestProfile.class)
@QuarkusTestResource(value = TestResourceLifecycleManager.class, initArgs = @ResourceArg(name=TestResourceLifecycleManager.EXTERNAL_AUTH_ARG, value="true"))
@TestHTTPEndpoint(Auth.class)
class AuthExternalTest extends RunningServerTest {

ExternalAuthTestProfile 对此进行了扩展,提供了适当的配置文件名称:

public class NonDefaultTestProfile implements QuarkusTestProfile {

    private final String testProfile;
    private final Map<String, String> overrides = new HashMap<>();

    protected NonDefaultTestProfile(String testProfile) {
        this.testProfile = testProfile;
    }

    protected NonDefaultTestProfile(String testProfile, Map<String, String> configOverrides) {
        this(testProfile);
        this.overrides.putAll(configOverrides);
    }

    @Override
    public Map<String, String> getConfigOverrides() {
        return new HashMap<>(this.overrides);
    }

    @Override
    public String getConfigProfile() {
        return testProfile;
    }

    @Override
    public List<TestResourceEntry> testResources() {
        return QuarkusTestProfile.super.testResources();
    }
}

生命周期管理器:

@Slf4j
public class TestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager {
    public static final String EXTERNAL_AUTH_ARG = "externalAuth";

    private static volatile MongodExecutable MONGO_EXE = null;
    private static volatile KeycloakContainer KEYCLOAK_CONTAINER = null;

    private boolean externalAuth = false;

    public synchronized Map<String, String> startKeycloakTestServer() {
        if(!this.externalAuth){
            log.info("No need for keycloak.");
            return Map.of();
        }
        if (KEYCLOAK_CONTAINER != null) {
            log.info("Keycloak already started.");
        } else {
            KEYCLOAK_CONTAINER = new KeycloakContainer()
//              .withEnv("hello","world")
                    .withRealmImportFile("keycloak-realm.json");
            KEYCLOAK_CONTAINER.start();
            log.info(
                    "Test keycloak started at endpoint: {}\tAdmin creds: {}:{}",
                    KEYCLOAK_CONTAINER.getAuthServerUrl(),
                    KEYCLOAK_CONTAINER.getAdminUsername(),
                    KEYCLOAK_CONTAINER.getAdminPassword()
            );

        }
        String clientId;
        String clientSecret;
        String publicKey = "";
        try (
                Keycloak keycloak = KeycloakBuilder.builder()
                        .serverUrl(KEYCLOAK_CONTAINER.getAuthServerUrl())
                        .realm("master")
                        .grantType(OAuth2Constants.PASSWORD)
                        .clientId("admin-cli")
                        .username(KEYCLOAK_CONTAINER.getAdminUsername())
                        .password(KEYCLOAK_CONTAINER.getAdminPassword())
                        .build();
        ) {
            RealmResource appsRealmResource = keycloak.realms().realm("apps");

            ClientRepresentation qmClientResource = appsRealmResource.clients().findByClientId("quartermaster").get(0);

            clientSecret = qmClientResource.getSecret();

            log.info("Got client id \"{}\" with secret: {}", "quartermaster", clientSecret);

            //get private key
            for (KeysMetadataRepresentation.KeyMetadataRepresentation curKey : appsRealmResource.keys().getKeyMetadata().getKeys()) {
                if (!SIG.equals(curKey.getUse())) {
                    continue;
                }
                if (!"RSA".equals(curKey.getType())) {
                    continue;
                }
                String publicKeyTemp = curKey.getPublicKey();
                if (publicKeyTemp == null || publicKeyTemp.isBlank()) {
                    continue;
                }
                publicKey = publicKeyTemp;
                log.info("Found a relevant key for public key use: {} / {}", curKey.getKid(), publicKey);
            }
        }
        // write public key
        // = new File(TestResourceLifecycleManager.class.getResource("/").toURI().toString() + "/security/testKeycloakPublicKey.pem");
        File publicKeyFile;
        try {
            publicKeyFile = File.createTempFile("oqmTestKeycloakPublicKey",".pem");
//            publicKeyFile = new File(TestResourceLifecycleManager.class.getResource("/").toURI().toString().replace("/classes/java/", "/resources/") + "/security/testKeycloakPublicKey.pem");
            log.info("path of public key: {}", publicKeyFile);
//            if(publicKeyFile.createNewFile()){
//                log.info("created new public key file");
//
//            } else {
//                log.info("Public file already exists");
//            }
            try (
                    FileOutputStream os = new FileOutputStream(
                            publicKeyFile
                    );
            ) {
                IOUtils.write(publicKey, os, UTF_8);
            } catch (IOException e) {
                log.error("Failed to write out public key of keycloak: ", e);
                throw new IllegalStateException("Failed to write out public key of keycloak.", e);
            }
        } catch (IOException  e) {
            log.error("Failed to create public key file: ", e);
            throw new IllegalStateException("Failed to create public key file", e);
        }

        String keycloakUrl = KEYCLOAK_CONTAINER.getAuthServerUrl().replace("/auth", "");

        return Map.of(
                "test.keycloak.url", keycloakUrl,
                "test.keycloak.authUrl", KEYCLOAK_CONTAINER.getAuthServerUrl(),
                "test.keycloak.adminName", KEYCLOAK_CONTAINER.getAdminUsername(),
                "test.keycloak.adminPass", KEYCLOAK_CONTAINER.getAdminPassword(),
                //TODO:: add config for server to talk to
                "service.externalAuth.url", keycloakUrl,
                "mp.jwt.verify.publickey.location", publicKeyFile.getAbsolutePath()


        );
    }

    public static synchronized void startMongoTestServer() throws IOException {
        if (MONGO_EXE != null) {
            log.info("Flapdoodle Mongo already started.");
            return;
        }
        Version.Main version = Version.Main.V4_0;
        int port = 27018;
        log.info("Starting Flapdoodle Test Mongo {} on port {}", version, port);
        IMongodConfig config = new MongodConfigBuilder()
                .version(version)
                .net(new Net(port, Network.localhostIsIPv6()))
                .build();
        try {
            MONGO_EXE = MongodStarter.getDefaultInstance().prepare(config);
            MongodProcess process = MONGO_EXE.start();
            if (!process.isProcessRunning()) {
                throw new IOException();
            }
        } catch (Throwable e) {
            log.error("FAILED to start test mongo server: ", e);
            MONGO_EXE = null;
            throw e;
        }
    }

    public static synchronized void stopMongoTestServer() {
        if (MONGO_EXE == null) {
            log.warn("Mongo was not started.");
            return;
        }
        MONGO_EXE.stop();
        MONGO_EXE = null;
    }

    public synchronized static void cleanMongo() throws IOException {
        if (MONGO_EXE == null) {
            log.warn("Mongo was not started.");
            return;
        }

        log.info("Cleaning Mongo of all entries.");
    }


    @Override
    public void init(Map<String, String> initArgs) {
        this.externalAuth = Boolean.parseBoolean(initArgs.getOrDefault(EXTERNAL_AUTH_ARG, Boolean.toString(this.externalAuth)));
    }

    @Override
    public Map<String, String> start() {
        log.info("STARTING test lifecycle resources.");
        Map<String, String> configOverride = new HashMap<>();
        try {
            startMongoTestServer();
        } catch (IOException e) {
            log.error("Unable to start Flapdoodle Mongo server");
        }

        configOverride.putAll(startKeycloakTestServer());

        return configOverride;
    }

    @Override
    public void stop() {
        log.info("STOPPING test lifecycle resources.");
        stopMongoTestServer();
    }
}

该应用程序可以在这里找到:https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/tree/main/software/open-qm-base-station

测试目前以我描述的方式失败,所以请随意环顾四周。

请注意,要运行它,您需要在https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/tree/main/software/libs/open-qm-core 中运行./gradlew build publishToMavenLocal 以在本地安装依赖项。

Github 问题也跟踪这个:https://github.com/quarkusio/quarkus/issues/22025

【问题讨论】:

  • 这听起来像是一个错误,但我不能确定......我建议在 Github 上打开一个问题,这样我们就可以确保我们不会丢失它
  • @geoand 谢谢,提出了一个问题。有问题的链接。

标签: java unit-testing automated-tests quarkus


【解决方案1】:

在@QuarkusTestResource()未将restrictToAnnotatedClass设置为true的情况下,任何使用@QuarkusTestResource()都意味着QuarkusTestResourceLifecycleManager将应用于所有测试,无论注释放置在何处。

希望restrictToAnnotatedClass能解决问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-11
    • 1970-01-01
    • 2016-06-30
    相关资源
    最近更新 更多