ウェルスナビ開発者ブログ

WealthNaviの開発に関する記事を書いてます。

Keycloakのテストについて

はじめに

こんにちは。ウェルスナビでフルスタックエンジニアの高原です。 前回の記事はKeycloak のカスタマイズについて解説しました。
続きまして、今回は Keycloak のテストについて説明します。

対象の読者

以下のような方を想定しています。

  • バックエンドエンジニア
  • フロントエンドエンジニア
  • インフラエンジニア
  • DevOps

背景

前回の記事で説明した通り、Keycloak はSPI (Service Provider Interface)を実装することで様々なユースケースに対応できるよう各処理をカスタマイズできます。そして、アプリケーションをデプロイする際には、Keycloak の Docker イメージが必要です。そのため、テストを行う際にも Keycloak の Docker 環境を設定する必要があります。
この問題を解決するために、Keycloak の testcontainerを導入します。これにより、テスト実行時に Keycloak のイメージを簡単に設定できます。また、外部システム API リクエストをモックするため、MockServerを導入し、モックレスポンスを作成できます。
この記事では、Keycloak のテスト環境を構築し、外部システムと連携やアプリケーションの挙動をテストする手法について説明します。

テスト環境を構築

テストコードの構築

前回作成したextensionsディレクトリにtestコードを追加します。

テストコードの構築

keycloak_customize
    ├── ...
    ├── extensions
    │    ├── src
    │    │    ├── main
    │    │    └── test
    │    │          ├── java
    │    │          │    └── com.wealthnavi.keycloak
    │    │          │         ├── KeycloakIntegrationTest.java (テスト実行用のクラス)
    │    │          │         └── KeycloakTestSupport.java    (テストのサポートクラス)
    │    │          └── resources
    │    │               ├── log4j.properties                   (ログの設定)
    │    │               ├── realm-export.json                  (テスト用のレルム設定)
    │    │               └── testcontainers.properties          (テストコンテナの設定)
    │    └── pom.xml                                            (ライブラリの定義ファイル)
    ├── ...

ライブラリを導入

まずはpom.xmlにテスト用のライブラリを導入します。

POMファイルを設定

<!-- Keycloakのテストコンテナ -->
<dependency>
    <groupId>com.github.dasniko</groupId>
    <artifactId>testcontainers-keycloak</artifactId>
    <version>${keycloak.testcontainer.version}</version>
    <scope>test</scope>
</dependency>

<!-- モックサーバー -->
<dependency>
    <groupId>org.mock-server</groupId>
    <artifactId>mockserver-netty-no-dependencies</artifactId>
    <version>${mockserver.version}</version>
    <scope>test</scope>
</dependency>

<!-- テスト用のユーティリティー-->
<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>${rest-assured.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>${common.io.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>${slf4j.version}</version>
    <scope>test</scope>
</dependency>

<!-- JUnitとassertライブラリ -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>${junit.jupiter.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>${assertj.version}</version>
    <scope>test</scope>
</dependency>

環境構築

ネットワークを作成

Keycloak のカスタマイズをデプロイする際、ネットワークの構成は次の通りです。

そのため、各コンポーネントをテスト用に設定します。

  • モックサーバーのテストコンテナ
  • メールサーバーのテストコンテナ
  • Keycloak のテストコンテナ

テストコードのKeycloakTestSupport.javaにネットワーク定義を追加します。

ネットワークを作成

@Slf4j
class KeycloakTestSupport {

    private static final Network network = Network.newNetwork();
    ...
}

モックサーバーのテストコンテナを作成

次に、モックサーバーのテストコンテナを作成します。
Docker イメージのバージョンは pom ファイルのmockserver.versionと同じにします。
先ほど作成したネットワークを使用します。
KeycloakTestSupport.javaファイルに以下の定義を追加します。

モックサーバーのテストコンテナを作成

private static final String mockServerAlias = "mockserver";

private static final int mockServerPort = 1080;

private static final List<MockServerClient> mockServerClients = new ArrayList<>();

private static final MockServerContainer mockServer = new MockServerContainer(DockerImageName.parse("mockserver/mockserver:5.15.0"))
        .withNetwork(network)
        .withNetworkAliases(mockServerAlias)
        .withExposedPorts(mockServerPort)
        .withReuse(true);

private static final String mockServerUrl = "http://" + mockServerAlias + ":" + mockServerPort;

メールサーバーのテストコンテナを作成

アプリをデプロイする際、MailHogを使用するため、テスト時にMailHogのテストコンテナを作成します。 モックサーバーの同じネットワーク構成を利用します。 KeycloakTestSupport.javaファイルに以下の定義を追加します。

メールサーバーのテストコンテナを作成

private static final String mailHogServerAlias = "mailhog";

private static final int mailHogServerPort = 1025;

private static final int mailHogServerWebPort = 8025;

private static final GenericContainer<?> mailhogServer = new GenericContainer<>("mailhog/mailhog")
        .withNetwork(network)
        .withNetworkAliases(mailHogServerAlias)
        .withExposedPorts(mailHogServerPort, mailHogServerWebPort)
        .withReuse(true);

Keycloak のテストコンテナを作成

まず、テスト用のレルムを設定しましょう。 Keycloak のカスタマイズをデプロイした後、レルムの設定ファイルを出力します。

  • Keycloak Admin Consoleにアクセスし、管理者権限でログインします。
  • テスト用のtestレルムに切り替え、ConfigureからRealm settingsに移動します。
  • Actionをクリックし、 Partial exportを選択します。
  • 表示するポップアップにInclude clientsを有効にし、 Exportボタンをクリックし、ファイルを出力します。

次は、出力したファイルの設定を変更します。

  • ファイルを開き、以下のテストユーザーを追加します。

テストユーザーを追加

"users": [
    {
        "username": "taro",
        "enabled": true,
        "email": "taro@example.com",
        "firstName": "Taro",
        "lastName": "",
        "credentials": [
        {
            "type": "password",
            "value": "taro123456"
        }
        ],
        "realmRoles": [
            "user",
            "offline_access"
        ],
        "clientRoles": {
            "account": [
                "manage-account"
            ]
        }
    },
    {
        "username": "test1",
        "enabled": true,
        "email": "test1@example.com",
        "firstName": "Test1",
        "lastName": "",
        "credentials": [
        {
            "type": "password",
            "value": "test123456"
        }
        ],
        "realmRoles": [
            "user",
            "offline_access"
        ],
        "clientRoles": {
            "account": [
                "manage-account"
            ]
        }
    }
]

  • authenticatorConfigの一覧を開き、"alias": "External Authenticator"アイテムのconfigを以下のように変更します。

認証部分を設定

"config": {
    "risk-analyze-api-url": "http://mockserver:1080/api/v1/wealthnavi/risk_analyze",
    "wealthnavi-check-api-url": "http://mockserver:1080/api/v1/wealthnavi/verify_user"
}

  • 次に、"alias": "custom registration with verify WealthNavi user existence",アイテムのconfigを以下のように変更します。

登録部分を設定

"config": {
    "wealthnavi-create-user-api-url": "http://mockserver:1080/api/v1/wealthnavi/create_user",
    "wealthnavi-check-api-url": "http://mockserver:1080/api/v1/wealthnavi/verify_user"
}

  • 最後に、これらの変更をテストリソースディレクトリに保存します。 Keycloak のテストコンテナを作成する際には、モックサーバーテストコンテナと MailHog テストコンテナも同じネットワーク構成を利用します。さらに、外部システムの API エンドポイントについては、モックサーバー用のエンドポイントを使用します。 KeycloakTestSupport.javaファイルに以下の定義を追加します。

Keycloakのテストコンテナを作成

private static final KeycloakContainer keycloakContainer = new KeycloakContainer()
            .withNetwork(network)
            .withRealmImportFile("realm-export.json")
            .withProviderClassesFrom("target/classes")
            .withEnv("KC_SPI_REQUIRED_ACTION_TERMS_AND_CONDITIONS_ACTION_WEALTHNAVI_UPDATE_TERMS_AND_CONDITIONS_API_URL", mockServerUrl + "/api/v1/wealthnavi/update_terms_and_conditions")
            .withEnv("KC_SPI_REQUIRED_ACTION_UPDATE_PASSWORD_ACTION_WEALTHNAVI_UPDATE_PASSWORD_API_URL", mockServerUrl + "/api/v1/wealthnavi/update_password")
            .withEnv("KC_SPI_REQUIRED_ACTION_VERIFY_EMAIL_BY_CODE_ACTION_WEALTHNAVI_UPDATE_STATUS_API_URL", mockServerUrl + "/api/v1/wealthnavi/update_status")
            .withReuse(true);

テストコードを書く

実行前、実行後の設定

テストを書く際には、@BeforeAll と@AfterAll を使用して、テストの実行前と実行後に設定することが多いです。そのため、KeycloakTestSupport.javaファイルにsetupstopメソッドを実装します。 setupメソッドでは Keycloak の各フローを設定し、さらにモックサーバテストコンテナやMailHogテストコンテナを起動します。そして、stopメソッドでは、各テストコンテナを停止させます。

テストの実行前、実行後の設定

private static final String realmName = "test";

private static final String browserFlow = "Custom browser";

private static final String registrationFlow = "Custom registration";

private static final String resetCredentialsFlow = "Custom reset credentials";

static void setup() {
    log.info("set up flows for keycloak container");
    keycloakContainer.start();
    keycloakContainer.followOutput(new Slf4jLogConsumer(log));
    var keycloakAdminClient = keycloakContainer.getKeycloakAdminClient();
    var realm = keycloakAdminClient.realm(realmName);
    var realmPresentation = realm.toRepresentation();
    realmPresentation.setBrowserFlow(browserFlow);
    realmPresentation.setRegistrationFlow(registrationFlow);
    realmPresentation.setResetCredentialsFlow(resetCredentialsFlow);
    realm.update(realmPresentation);
    keycloakAdminClient.close();

    log.info("create mock server client");
    mockServer.start();
    mockServer.followOutput(new Slf4jLogConsumer(log));

    log.info("create mailhog server client");
    mailhogServer.start();
    mailhogServer.followOutput(new Slf4jLogConsumer(log));
}

static void stop() {
    log.info("stopMockServer");
    mockServerClients.forEach(MockServerClient::stop);
    mockServerClients.clear();
    mockServer.stop();
    log.info("stopKeycloakContainer");
    keycloakContainer.stop();
    log.info("stopMailhogServer");
    mailhogServer.stop();
}

登録フローのテスト

最初に、ユーザー登録フローのテストを実施します。テストを開始する前に、フローのシーケンスを想定しましょう。以下にその手順を示します。

外部システムのモックレスポンスを作成

登録シーケンス図から、外部システムが以下の処理を行います。

  • ユーザー存在確認
  • 新規ユーザーを作成
  • メール認証のステータスを更新
  • 利用規約の承諾ステータスを更新

上記の処理に対応するため、各リクエストに合わせて、モックレスポンスを作成します。 KeycloakTestSupport.javaファイルにcreateRegistrationMockResponsesメソッドを追加します。

モックレスポンスを作成

static void createRegistrationMockResponses() {
    log.info("create registration mock responses");
    mockServer.stop();
    mockServerClients.clear();

    mockServer.start();
    mockServer.followOutput(new Slf4jLogConsumer(log));

    var mockVerifyExistedUserClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockVerifyExistedUserClient);
    mockVerifyExistedUserClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/verify_user")
                    .withBody(json("{\"emailAddress\" : \"test1@example.com\"}", MatchType.STRICT))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isWealthNaviUser\": true}", MediaType.APPLICATION_JSON)
    );

    var mockVerifyNotExistedUserClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockVerifyNotExistedUserClient);
    mockVerifyNotExistedUserClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/verify_user")
                    .withBody(json("{\"emailAddress\" : \"test@example.com\"}", MatchType.STRICT))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isWealthNaviUser\": false}", MediaType.APPLICATION_JSON)
    );

    var mockCreateUserClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockCreateUserClient);
    mockCreateUserClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/create_user")
                    .withBody(json("{\"email\" : \"test@example.com\"}", MatchType.ONLY_MATCHING_FIELDS))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isSuccess\": true}", MediaType.APPLICATION_JSON)
    );

    var mockCreateUserFailedClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockCreateUserFailedClient);
    mockCreateUserClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/create_user")
                    .withBody(json("{\"email\" : \"test2@example.com\"}", MatchType.ONLY_MATCHING_FIELDS))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isSuccess\": false}", MediaType.APPLICATION_JSON)
    );

    var mockUpdateStatusClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockUpdateStatusClient);
    mockUpdateStatusClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/update_status")
                    .withBody(json("{\"email\" : \"test@example.com\", \"isVerified\": true}", MatchType.ONLY_MATCHING_FIELDS))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isSuccess\": true}", MediaType.APPLICATION_JSON)
    );

    var mockUpdateTermClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockUpdateTermClient);
    mockUpdateTermClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/update_terms_and_conditions")
                    .withBody(json("{\"isAgreed\": true}", MatchType.ONLY_MATCHING_FIELDS))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isSuccess\": true}", MediaType.APPLICATION_JSON)
    );
}

登録のテストコードを書く

登録シーケンス図に基づき、ユーザー側の手順を想定します。

  1. Keycloak のログインページを開き、登録ボタンをクリック、登録ページに移動
  2. ユーザーの登録情報を入力して、登録ボタンをクリック、登録リクエストを送信
  3. 認証コード画面が表示され、メールサーバーをアクセスし、コードを入力
  4. 利用規約の承諾画面が表示され、承諾して、登録完了

上記の想定に基づき、各ステップのテストコードを書いていきます。

1. 登録ページのリンクを取得

OpenID の標準に基づき、ログインのbase urlを利用して Keycloak の設定ファイルにアクセスし、authorization_endpoint項目を取得します。 KeycloakIntegrationTest.javaファイルに以下の実装を追加します。

ログインリンクを取得

private static String getOpenIdConfigurationUrl(String config) {
    return given()
            .get(KeycloakTestSupport.configurationUrl())
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path(config);
}

private static String getLoginUrl() {
    return getOpenIdConfigurationUrl("authorization_endpoint");
}

configurationUrlKeycloakTestSupport.javaに定義されます。

configurationUrlの定義

static String configurationUrl() {
    return keycloakContainer.getAuthServerUrl() + "/realms/" + realmName + "/.well-known/openid-configuration";
}

ログインのbase urlを取得した後、テスト用のfront_endクライアントでリクエストし、HTML レスポンスを受け取ります。そこからregexを利用して登録ページのリンクを取得します。 KeycloakIntegrationTest.javaファイルに以下の実装を追加します。

認証リンクを取得

private static Response openLoginPage(String loginUrl) {
    return given()
            .headers(KeycloakTestSupport.getHeaders())
            .queryParams(KeycloakTestSupport.getLoginQueryParams())
            .get(loginUrl)
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .response();
}

private static String getAuthLinks(String type, String pageResponse) {
    if ("login".equals(type)) return KeycloakTestSupport.extractLink("action=\"([^\"]*)\"", pageResponse, 1);
    else if ("register".equals(type))
        return KeycloakTestSupport.extractLink("href=\"(/auth)?(/realms/[^\"]*/login-actions/registration[^\"]*)\"", pageResponse, 2);
    else if ("reset-password".equals(type))
        return KeycloakTestSupport.extractLink("href=\"(/auth)?(/realms/[^\"]*/login-actions/reset-credentials[^\"]*)\"", pageResponse, 2);
    else return null;
}

getHeadersgetLoginQueryParamsKeycloakTestSupport.javaに定義されます。

getHeadersとgetLoginQueryParamsの定義

static final String redirectUri = "http://localhost:8080";

static Map<String, String> getHeaders() {
    return new HashMap<>() {
        @Serial
        private static final long serialVersionUID = -3042611989483299865L;

        {
            put("Accept", "text/html,application/xhtml+xml,application/xml");
            put("Accept-Encoding", "gzip, deflate");
            put("Accept-Language", "en-US,en;q=0.5");
            put("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0");
        }
    };
}

static Map<String, String> getLoginQueryParams() {
    return new HashMap<>() {
        @Serial
        private static final long serialVersionUID = -2967571483916088610L;

        {
            put("login", "true");
            put("response_type", "code");
            put("client_id", "front_end");
            put("state", UUID.randomUUID().toString());
            put("redirect_uri", KeycloakTestSupport.redirectUri);
            put("scope", "openid");
        }
    };
}

まとめて、KeycloakIntegrationTest.javaファイルにtestRegistrationの実装を追加します。

登録ページのリンクを取得

@Test
void testRegistration() {
    log.info("test registration");

    KeycloakTestSupport.createRegistrationMockResponses();
    String loginUrl = getLoginUrl();

    // get register link
    Response loginPageResponse = openLoginPage(loginUrl);
    String registerLink = KeycloakTestSupport.getBaseUrl() + getAuthLinks("register", loginPageResponse.asString());
    log.info("registerLink: {}", registerLink);

    ...
}

getBaseUrlKeycloakTestSupport.javaに定義されます。

getBaseUrlの定義

static String getBaseUrl() {
    return keycloakContainer.getAuthServerUrl();
}

2. 登録リクエストを送信

登録ページの HTML レスポンスを受け取り、それからregexを利用して、登録リンクを取得します。 ユーザー情報を登録した後、登録リクエストを送信します。Keycloak はセキュリティを強化するため、パラメータはこの段階で code 要素に設定されていますので、同様に cookies も統一されます。 KeycloakIntegrationTest.javaファイルに以下の実装を追加します。

登録リクエストを送信

private static Response openPage(String url, Map<String, String> cookies) {
    return given()
            .cookies(cookies)
            .headers(KeycloakTestSupport.getHeaders())
            .get(url)
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .response();
}

private static Response register(String registerLink, Map<String, String> cookies, String firstName, String lastName, String email, String userName, String password) {
    Response registerPageResponse = openPage(registerLink, cookies);
    String registerPageLink = getAuthLinks("login", registerPageResponse.asString());
    log.info("registerPageLink: {}", registerPageLink);
    return given()
            .cookies(cookies)
            .headers(KeycloakTestSupport.getHeaders())
            .formParam("firstName", firstName)
            .formParam("lastName", lastName)
            .formParam("email", email)
            .formParam("username", userName)
            .formParam("password", password)
            .formParam("password-confirm", password)
            .post(registerPageLink);
}

まとめて、KeycloakIntegrationTest.javaファイルにtestRegistrationの実装を追加します。 登録が成功すると、コード認証画面に移動し、レスポンスからLocationヘッダーに含まれるコード入力するページのリンクを取得できます。

登録リクエストを検証

@Test
void testRegistration() {
    ...

    // test return error if required fields are empty
    register(registerLink, loginPageResponse.cookies(), "", "", "", "", "")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.INVALID_REGISTRATION);

    // test return error if username is existed
    register(registerLink, loginPageResponse.cookies(), "taro", "test", "test@example.com", "taro", "test12345678")
            .then()
            .statusCode(HttpStatusCode.BAD_REQUEST_400.code())
            .extract()
            .path("errorMessage", Errors.USERNAME_IN_USE);

    // test return error is user is existed
    register(registerLink, loginPageResponse.cookies(), "test1", "test1", "test1@example.com", "test1", "test12345678")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.EMAIL_IN_USE);

    // test return error if cannot create user in external system
    register(registerLink, loginPageResponse.cookies(), "test2", "test2", "test2@example.com", "test2", "test12345678")
            .then()
            .statusCode(HttpStatusCode.BAD_REQUEST_400.code())
            .extract()
            .path("errorMessage", Errors.INVALID_REGISTRATION);

    // test redirect if registration is successful
    Response successRegisterResponse = register(registerLink, loginPageResponse.cookies(), "test", "test", "test@example.com", "test", "test123456");
    successRegisterResponse.then()
            .statusCode(HttpStatusCode.FOUND_302.code());
    String submitCodeUrl = successRegisterResponse.header("Location");
    log.info("submitCodePage: {}", submitCodeUrl);

    ...
}

3. 認証コードを検証

コード入力ページの HTML レスポンスを受け取り、それからregexを利用して、コード認証リンクが取得できます。 さらに、コードを取得するために、MailHogサーバーの API を使用して、認証コードが含まれたメールにアクセスできます。その後、regexを使用してコードを取得します。 KeycloakIntegrationTest.javaファイルにgetVerifyEmailCodeの実装を追加します。

認証コードを取得

 private static String getVerifyEmailCode() {
    Response mailListResponse = given()
            .when()
            .get(KeycloakTestSupport.getMailListApiUrl());
    JsonPath jsonResponse = mailListResponse.jsonPath();
    assert jsonResponse.getInt("total") == 1;
    String emailContent = jsonResponse.getString("items[0].MIME.Parts[0].Body");
    return KeycloakTestSupport.extractCode(emailContent, ".*\\r\\n\\r\\n(.*)\\r\\n\\r\\n.*");
}

getMailListApiUrlextractCodeKeycloakTestSupport.javaに定義されます。

getMailListApiUrlとextractCodeの定義

static String getMailListApiUrl() {
    return "http://" + mailhogServer.getHost() + ":" + mailhogServer.getMappedPort(mailHogServerWebPort) + "/api/v2/messages";
}

static String extractCode(String input, String pattern) {
    try {
        Pattern patternReg = Pattern.compile(pattern);
        Matcher matcher = patternReg.matcher(input);
        if (matcher.matches()) return matcher.group(1);
        return null;
    } catch (Exception e) {
        log.error("error", e);
        return null;
    }
}

取得したコードを利用して、先ほどのコード認証リンクへリクエストします。

認証コードを送信

private static Response submitCode(String code, String submitCodeLink, Map<String, String> cookies) {
    return given()
            .cookies(cookies)
            .formParam("email_code", code)
            .post(submitCodeLink);
}

まとめて、KeycloakIntegrationTest.javaファイルにtestRegistrationの実装を追加します。

認証コードを検証

@Test
void testRegistration() {
    ...

    String submitPageResponse = openPage(submitCodeUrl, loginPageResponse.cookies())
            .then()
            .extract()
            .response().asString();
    // test received verify email if registration is successful
    String code = getVerifyEmailCode();
    String submitCodeLink = getAuthLinks("login", submitPageResponse);
    log.info("submitCodeLink: {} with code:{}", submitCodeLink, code);
    // test submit correct code
    submitCode(code, submitCodeLink, loginPageResponse.cookies())
            .then()
            .statusCode(HttpStatusCode.OK_200.code());

    ...
}

4. 利用規約を承諾

認証コードを確認すると、利用規約を承諾する画面が表示されます。承諾すると、登録が完了します。 KeycloakIntegrationTest.javaファイルにtestRegistrationの実装を追加します。

利用規約を承諾

@Test
void testRegistration() {
    ...

    String updateTermUrl = submitCodeUrl.replaceAll("verify-email-by-code-action", "terms-and-conditions-action");
    log.info("updateTermPage: {}", updateTermUrl);
    String updateTermPageResponse = openPage(updateTermUrl, loginPageResponse.cookies())
            .then()
            .extract()
            .response().asString();
    String updateTermLink = getAuthLinks("login", updateTermPageResponse);
    log.info("updateTermLink: {}", updateTermLink);
    // test agree terms and conditions
    given()
            .cookies(loginPageResponse.getCookies())
            .headers(KeycloakTestSupport.getHeaders())
            .post(updateTermLink)
            .then()
            .statusCode(HttpStatusCode.FOUND_302.code())
            .header("Location", containsString(KeycloakTestSupport.redirectUri));
}

上記の 4 つのステップをまとめると、testRegistration の実装は以下のようになります。

testRegistrationのまとめ

@Test
void testRegistration() {
    log.info("test registration");

    KeycloakTestSupport.createRegistrationMockResponses();
    String loginUrl = getLoginUrl();

    // get register link
    Response loginPageResponse = openLoginPage(loginUrl);
    String registerLink = KeycloakTestSupport.getBaseUrl() + getAuthLinks("register", loginPageResponse.asString());
    log.info("registerLink: {}", registerLink);

    // test return error if required fields are empty
    register(registerLink, loginPageResponse.cookies(), "", "", "", "", "")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.INVALID_REGISTRATION);

    // test return error if username is existed
    register(registerLink, loginPageResponse.cookies(), "taro", "test", "test@example.com", "taro", "test12345678")
            .then()
            .statusCode(HttpStatusCode.BAD_REQUEST_400.code())
            .extract()
            .path("errorMessage", Errors.USERNAME_IN_USE);

    // test return error is user is existed
    register(registerLink, loginPageResponse.cookies(), "test1", "test1", "test1@example.com", "test1", "test12345678")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.EMAIL_IN_USE);

    // test return error if cannot create user in external system
    register(registerLink, loginPageResponse.cookies(), "test2", "test2", "test2@example.com", "test2", "test12345678")
            .then()
            .statusCode(HttpStatusCode.BAD_REQUEST_400.code())
            .extract()
            .path("errorMessage", Errors.INVALID_REGISTRATION);

    // test redirect if registration is successful
    Response successRegisterResponse = register(registerLink, loginPageResponse.cookies(), "test", "test", "test@example.com", "test", "test123456");
    successRegisterResponse.then()
            .statusCode(HttpStatusCode.FOUND_302.code());
    String submitCodeUrl = successRegisterResponse.header("Location");
    log.info("submitCodePage: {}", submitCodeUrl);

    String submitPageResponse = openPage(submitCodeUrl, loginPageResponse.cookies())
            .then()
            .extract()
            .response().asString();
    // test received verify email if registration is successful
    String code = getVerifyEmailCode();
    String submitCodeLink = getAuthLinks("login", submitPageResponse);
    log.info("submitCodeLink: {} with code:{}", submitCodeLink, code);
    // test submit correct code
    submitCode(code, submitCodeLink, loginPageResponse.cookies())
            .then()
            .statusCode(HttpStatusCode.OK_200.code());

    String updateTermUrl = submitCodeUrl.replaceAll("verify-email-by-code-action", "terms-and-conditions-action");
    log.info("updateTermPage: {}", updateTermUrl);
    String updateTermPageResponse = openPage(updateTermUrl, loginPageResponse.cookies())
            .then()
            .extract()
            .response().asString();
    String updateTermLink = getAuthLinks("login", updateTermPageResponse);
    log.info("updateTermLink: {}", updateTermLink);
    // test agree terms and conditions
    given()
            .cookies(loginPageResponse.getCookies())
            .headers(KeycloakTestSupport.getHeaders())
            .post(updateTermLink)
            .then()
            .statusCode(HttpStatusCode.FOUND_302.code())
            .header("Location", containsString(KeycloakTestSupport.redirectUri));
}

認証フローのテスト

次に、認証フローのテストを実施します。テストを開始する前に、フローのシーケンスを想定しましょう。以下にその手順を示します。

外部システムのモックレスポンスを作成

認証シーケンス図から、外部システムが以下の処理を行います。

  • リスク分析(ログイン時に不正アクセスではないか)
  • ユーザー存在確認

上記の処理に対応するため、各リクエストに合わせて、モックレスポンスを作成します。 KeycloakTestSupport.javaファイルにcreateAuthenticateMockResponsesメソッドを追加します。

モックレスポンスを作成

static void createAuthenticateMockResponses() {
    log.info("create authenticate mock responses");
    mockServer.stop();
    mockServerClients.clear();

    mockServer.start();
    mockServer.followOutput(new Slf4jLogConsumer(log));

    var mockRiskAnalyzeClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockRiskAnalyzeClient);
    mockRiskAnalyzeClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/risk_analyze")
                    .withBody(json("{\"numOfLoginFailure\" : 0}", MatchType.ONLY_MATCHING_FIELDS))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"score\": \"LOW\"}", MediaType.APPLICATION_JSON)
    );

    var mockVerifyExistedUserClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockVerifyExistedUserClient);
    mockVerifyExistedUserClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/verify_user")
                    .withBody(json("{\"emailAddress\" : \"taro@example.com\"}", MatchType.STRICT))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isWealthNaviUser\": true}", MediaType.APPLICATION_JSON)
    );


    var mockVerifyNotExistedUserClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockVerifyNotExistedUserClient);
    mockVerifyNotExistedUserClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/verify_user")
                    .withBody(json("{\"emailAddress\" : \"test1@example.com\"}", MatchType.STRICT))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isWealthNaviUser\": false}", MediaType.APPLICATION_JSON)
    );
}

認証のテストコードを書く

認証シーケンス図に基づき、ユーザー側の手順を想定します。

  1. Keycloak のログインページを開き
  2. 認証情報を入力して、ログインボタンをクリック、認証リクエストを送信

想定した手順に基づいて、上記のステップのテストコードを書きます。 KeycloakTestIntegration.javaファイルにtestAuthenticationauthenticateの実装を追加します。

認証をテスト

@Test
void testAuthentication() {
    log.info("test authentication");

    KeycloakTestSupport.createAuthenticateMockResponses();
    String loginUrl = getLoginUrl();

    // test redirect if credentials are valid and user is existed in external system
    authenticate(loginUrl, "taro", "taro123456")
            .then()
            .statusCode(HttpStatusCode.FOUND_302.code())
            .header("Location", containsString(KeycloakTestSupport.redirectUri));

    // test not redirect if credentials are invalid (password is wrong)
    authenticate(loginUrl, "taro", "taro123456aaa")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.INVALID_USER_CREDENTIALS);

    // test not redirect if credentials are invalid (user is not existed)
    authenticate(loginUrl, "test", "aaaa")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.INVALID_USER_CREDENTIALS);

    // test return error if credentials are valid but user is not existed in external system
    authenticate(loginUrl, "test1", "test123456")
            .then()
            .statusCode(HttpStatusCode.BAD_REQUEST_400.code())
            .extract()
            .path("errorMessage", Errors.INVALID_USER_CREDENTIALS);
}

private static Response authenticate(String loginUrl, String userName, String password) {
    Response loginPageResponse = openLoginPage(loginUrl);
    String loginLink = getAuthLinks("login", loginPageResponse.asString());
    log.info("loginLink: {}", loginLink);

    return given()
            .headers(KeycloakTestSupport.getHeaders())
            .cookies(loginPageResponse.getCookies())
            .formParam("username", userName)
            .formParam("password", password)
            .when()
            .post(loginLink);
}

パスワードリセットフローのテスト

最後は、パスワードリセットフローのテストを実施します。テストを開始する前に、フローのシーケンスを想定しましょう。以下にその手順を示します。

外部システムのモックレスポンスを作成

登録シーケンス図から、外部システムがパスワードを更新する処理を行います。このために、モックレスポンスを作成します。 KeycloakTestSupport.javaファイルにcreateUpdatePasswordMockResponseメソッドを追加します。

モックレスポンスを作成

static void createUpdatePasswordMockResponse() {
    log.info("create update password mock responses");
    mockServer.stop();
    mockServerClients.clear();
    mailhogServer.stop();

    mockServer.start();
    mockServer.followOutput(new Slf4jLogConsumer(log));

    mailhogServer.start();
    mailhogServer.followOutput(new Slf4jLogConsumer(log));

    var mockUpdatePasswordClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    mockServerClients.add(mockUpdatePasswordClient);
    mockUpdatePasswordClient.when(
            request()
                    .withMethod("POST")
                    .withPath("/api/v1/wealthnavi/update_password")
                    .withBody(json("{\"email\": \"taro@example.com\"}", MatchType.ONLY_MATCHING_FIELDS))
    ).respond(
            response()
                    .withStatusCode(HttpStatusCode.OK_200.code())
                    .withBody("{\"isSuccess\": true}", MediaType.APPLICATION_JSON)
    );
}

パスワードリセットフローのテストコードを書く

パスワードリセットシーケンス図に基づき、ユーザー側の手順を想定します。

  1. Keycloak のログインページを開き、「パスワードをお忘れですか?」ボタンをクリック、ユーザー名またはメールアドレスを入力するページに移動
  2. ユーザーの情報を入力して、送信ボタンをクリック、認証コードが含まれたメールを送信
  3. 認証コード画面が表示され、メールサーバーにアクセスし、コードを入力
  4. パスワードを変更画面が表示され、パスワードを変更

想定した手順に基づいて、上記のステップのテストコードを書きます。

1. パスワードリセットリンクを取得

Keycloak のログインページを開き、HTML レスポンスを受け取ります。そこからregexを利用してパスワードリセットページのリンクを取得します。 KeycloakIntegrationTest.javaファイルにtestResetPasswordの実装を追加します。

パスワードリセットリンクを取得

@Test
void testResetPassword() {
    log.info("test reset password");

    KeycloakTestSupport.createUpdatePasswordMockResponse();
    String loginUrl = getLoginUrl();

    // get reset password link
    Response loginPageResponse = openLoginPage(loginUrl);
    String resetPasswordLink = KeycloakTestSupport.getBaseUrl() + getAuthLinks("reset-password", loginPageResponse.asString());
    log.info("resetPasswordLink: {}", resetPasswordLink);

    ...
}

2. 認証コードが含まれたメールを送信

上記のパスワードリセットページの HTML レスポンスを受け取り、それからregexを利用して、メールを送信リクエストリンクを取得します。 Keycloak はセキュリティを強化するため、パラメータはこの段階で code 要素に設定されていますので、同様に cookies も統一されます。 KeycloakIntegrationTest.javaファイルにtestResetPasswordsendResetPasswordEmailの実装を追加します。

メールを送信

@Test
void testResetPassword() {
    ...

    Response sendResetPasswordEmailResponse = sendResetPasswordEmail(resetPasswordLink, loginPageResponse.getCookies(), "taro");
    sendResetPasswordEmailResponse.then()
        .statusCode(HttpStatusCode.OK_200.code());

    ...
}

private static Response sendResetPasswordEmail(String resetPasswordLink, Map<String, String> cookies, String username) {
    Response resetPasswordPageResponse = openPage(resetPasswordLink, cookies);
    String sendResetEmailLink = getAuthLinks("login", resetPasswordPageResponse.asString());
    log.info("resetPasswordPageLink: {}", sendResetEmailLink);
    return given()
            .cookies(cookies)
            .headers(KeycloakTestSupport.getHeaders())
            .formParam("username", username)
            .post(sendResetEmailLink);
}

3. 認証コードを検証

メール送信リクエストリンクの HTML レスポンスを受け取り、それからregexを利用して、コード認証リンクが取得できます。 さらに、コードを取得するために、MailHogサーバーの API を使用して、認証コードが含まれたメールにアクセスできます。その後、regexを使用してコードを取得します。 取得したコードを利用して、先ほどのコード認証リンクへリクエストします。 まとめて、KeycloakIntegrationTest.javaファイルにtestResetPasswordの実装を追加します。

認証コードを検証

@Test
void testResetPassword() {
    ...

    String submitCodeLink = getAuthLinks("login", sendResetPasswordEmailResponse.asString());
    String code = getVerifyEmailCode();
    // test submit correct code
    log.info("submitCodeLink: {} with code:{}", submitCodeLink, code);
    Response submitCodeResponse = submitCode(code, submitCodeLink, loginPageResponse.cookies());
 submitCodeResponse.then().statusCode(HttpStatusCode.FOUND_302.code());

    ...
}

4. パスワードを更新

認証コードが確認できると、パスワード変更画面が表示されます。コードの認証レスポンスのLocationヘッダーからパスワード更新ページのリンクが取得できます。 続いて、パスワード更新ページでパスワードを更新します。 まとめて、KeycloakIntegrationTest.javaファイルにtestResetPasswordupdatePasswordの実装を追加します。

パスワード更新を検証

@Test
void testResetPassword() {
    ...

    String updatePasswordPage = submitCodeResponse.header("Location");
    log.info("updatePasswordPage: {}", updatePasswordPage);
    // test empty password
    updatePassword(updatePasswordPage, loginPageResponse.cookies(), "", "")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.PASSWORD_MISSING);
    // test update password not match
    updatePassword(updatePasswordPage, loginPageResponse.cookies(), "wealth0428", "wealth0428a")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.PASSWORD_CONFIRM_ERROR);
    // test update password
    updatePassword(updatePasswordPage, loginPageResponse.cookies(), "wealth0428", "wealth0428")
            .then()
            .statusCode(HttpStatusCode.FOUND_302.code())
            .headers("Location", containsString(KeycloakTestSupport.redirectUri));
}

private static Response updatePassword(String updatePasswordPage, Map<String, String> cookies, String password, String confirmPassword) {
    Response updatePasswordPageResponse = openPage(updatePasswordPage, cookies);
    String updatePasswordLink = getAuthLinks("login", updatePasswordPageResponse.asString());
    log.info("updatePasswordLink: {}", updatePasswordLink);
    return given()
            .cookies(cookies)
            .headers(KeycloakTestSupport.getHeaders())
            .formParam("password-new", password)
            .formParam("password-confirm", confirmPassword)
            .post(updatePasswordLink);
}

上記の 4 つのステップをまとめると、testResetPassword の実装は以下のようになります。

testResetPasswordのまとめ

@Test
void testResetPassword() {
    log.info("test reset password");

    KeycloakTestSupport.createUpdatePasswordMockResponse();
    String loginUrl = getLoginUrl();

    // get reset password link
    Response loginPageResponse = openLoginPage(loginUrl);
    String resetPasswordLink = KeycloakTestSupport.getBaseUrl() + getAuthLinks("reset-password", loginPageResponse.asString());
    log.info("resetPasswordLink: {}", resetPasswordLink);

    Response sendResetPasswordEmailResponse = sendResetPasswordEmail(resetPasswordLink, loginPageResponse.getCookies(), "taro");
    sendResetPasswordEmailResponse.then()
        .statusCode(HttpStatusCode.OK_200.code());

    String submitCodeLink = getAuthLinks("login", sendResetPasswordEmailResponse.asString());
    String code = getVerifyEmailCode();
    // test submit correct code
    log.info("submitCodeLink: {} with code:{}", submitCodeLink, code);
    Response submitCodeResponse = submitCode(code, submitCodeLink, loginPageResponse.cookies());

    submitCodeResponse.then().statusCode(HttpStatusCode.FOUND_302.code());
    String updatePasswordPage = submitCodeResponse.header("Location");
    log.info("updatePasswordPage: {}", updatePasswordPage);
    // test empty password
    updatePassword(updatePasswordPage, loginPageResponse.cookies(), "", "")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.PASSWORD_MISSING);
    // test update password not match
    updatePassword(updatePasswordPage, loginPageResponse.cookies(), "wealth0428", "wealth0428a")
            .then()
            .statusCode(HttpStatusCode.OK_200.code())
            .extract()
            .path("errorMessage", Errors.PASSWORD_CONFIRM_ERROR);
    // test update password
    updatePassword(updatePasswordPage, loginPageResponse.cookies(), "wealth0428", "wealth0428")
            .then()
            .statusCode(HttpStatusCode.FOUND_302.code())
            .headers("Location", containsString(KeycloakTestSupport.redirectUri));
}

テストのコードカバー率レポート

テストを実行する際には、コードカバー率レポートが重要です。そのため、jacocoを導入します。 簡単に述べると、Keycloak のテストコンテナ内で Jacoco エージェントを実行し、テストが終了した後に生成されたファイルをホストマシンにコピーして、レポートを生成します。

POM ファイルを設定

まず、POM ファイルにjacocoの設定を追加します。

POMファイルを設定

<!-- Jacocoエージェントを追加 -->
<dependency>
      <groupId>org.jacoco</groupId>
      <artifactId>org.jacoco.agent</artifactId>
      <version>${jacoco.version}</version>
      <classifier>runtime</classifier>
      <scope>test</scope>
</dependency>

<!-- テストのプロフィールを設定 -->
<profiles>
    <profile>
        <id>with-integration-tests</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>${maven-failsafe-plugin.version}</version>
                    <executions>
                        <execution>
                            <phase>integration-test</phase>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <includes>
                            <include>**/*IntegrationTest.java</include>
                        </includes>
                    </configuration>
                </plugin>
                <!-- ビルドディレクトリにJacocoエージェントをコピー -->
                <plugin>
                    <artifactId>maven-dependency-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>copy-jacoco</id>
                            <goals>
                                <goal>copy-dependencies</goal>
                            </goals>
                            <phase>compile</phase>
                            <configuration>
                                <includeArtifactIds>org.jacoco.agent</includeArtifactIds>
                                <includeClassifiers>runtime</includeClassifiers>
                                <outputDirectory>${project.build.directory}/jacoco-agent</outputDirectory>
                                <stripVersion>true</stripVersion>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <!-- ビルドディレクトリにレポートディレクトリを作成 -->
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>${exec-maven-plugin.version}</version>
                    <executions>
                        <execution>
                            <id>create-directory</id>
                            <phase>compile</phase>
                            <goals>
                                <goal>exec</goal>
                            </goals>
                            <configuration>
                                <executable>mkdir</executable>
                                <arguments>
                                    <argument>-p</argument>
                                    <argument>${project.build.directory}/jacoco-report</argument>
                                </arguments>
                                <longModulepath>false</longModulepath>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <!-- Jacocoのレポートとルールを設定 -->
                <plugin>
                    <groupId>org.jacoco</groupId>
                    <artifactId>jacoco-maven-plugin</artifactId>
                    <version>${jacoco.version}</version>
                    <executions>
                        <execution>
                            <id>report</id>
                            <phase>integration-test</phase>
                            <goals>
                                <goal>report</goal>
                            </goals>
                            <configuration>
                                <dataFile>${project.build.directory}/jacoco-report/jacoco.exec</dataFile>
                            </configuration>
                        </execution>
                        <execution>
                            <id>check</id>
                            <phase>post-integration-test</phase>
                            <goals>
                                <goal>check</goal>
                            </goals>
                            <configuration>
                                <dataFile>${project.build.directory}/jacoco-report/jacoco.exec</dataFile>
                                <rules>
                                    <rule>
                                        <element>BUNDLE</element>
                                        <limits>
                                            <limit>
                                                <counter>LINE</counter>
                                                <value>COVEREDRATIO</value>
                                                <minimum>0.7</minimum>
                                            </limit>
                                            <limit>
                                                <counter>BRANCH</counter>
                                                <value>COVEREDRATIO</value>
                                                <minimum>0.5</minimum>
                                            </limit>
                                        </limits>
                                    </rule>
                                </rules>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Keycloak のテストコンテナを設定

テストコンテナが起動する時、jacocoが実行できるため、KeycloakTestSupport.javaの定義したkeycloakContainerを以下のように更新します。

Keycloakのテストコンテナ定義を更新

 private static final KeycloakContainer keycloakContainer = new KeycloakContainer()
    .withNetwork(network)
    .withRealmImportFile("realm-export.json")
    .withProviderClassesFrom("target/classes")
    .withEnv("KC_SPI_REQUIRED_ACTION_TERMS_AND_CONDITIONS_ACTION_WEALTHNAVI_UPDATE_TERMS_AND_CONDITIONS_API_URL", mockServerUrl + "/api/v1/wealthnavi/update_terms_and_conditions")
    .withEnv("KC_SPI_REQUIRED_ACTION_UPDATE_PASSWORD_ACTION_WEALTHNAVI_UPDATE_PASSWORD_API_URL", mockServerUrl + "/api/v1/wealthnavi/update_password")
    .withEnv("KC_SPI_REQUIRED_ACTION_VERIFY_EMAIL_BY_CODE_ACTION_WEALTHNAVI_UPDATE_STATUS_API_URL", mockServerUrl + "/api/v1/wealthnavi/update_status")
    .withReuse(true)
    .withCopyFileToContainer(
            MountableFile.forHostPath("target/jacoco-agent/"),
            "/jacoco-agent"
    )
    .withEnv("JAVA_OPTS_APPEND", "-javaagent:/jacoco-agent/org.jacoco.agent-runtime.jar=destfile=/tmp/jacoco.exec");

テスト実行後の設定

テストが完了した後に生成されたファイルをビルドディレクトリにコピーするため、KeycloakTestSupport.javastopメソッドを以下のように更新します。

テスト実行後の設定を更新

static void stop() {
    log.info("stopMockServer");
    mockServerClients.forEach(MockServerClient::stop);
    mockServerClients.clear();
    mockServer.stop();
    log.info("stopKeycloakContainer");
    keycloakContainer.getDockerClient().stopContainerCmd(keycloakContainer.getContainerId()).exec();
    keycloakContainer.copyFileFromContainer("/tmp/jacoco.exec", "./target/jacoco-report/jacoco.exec");
    keycloakContainer.stop();
    log.info("stopMailhogServer");
    mailhogServer.stop();
}

テストを実行

テストを実行する際は、以下のコマンドを実行します。

mvn clean verify -Pintegration-test

テストのコードカバー率レポート

テストを実行後、以下のコマンドを実行して、レポートが確認できます。

open extensions/target/site/jacoco/index.html

レポートは以下の通りです。

まとめ

本記事では、Keycloak カスタマイズのテスト周りについて紹介しました。 こちらを参考にすることで、CI/CD 戦略を設定できます。 Keycloak を導入検討する際の参考にしていただければ嬉しいです。


明日は、VPoE 和賀 の「CTOとVPoEへのキャリアアップについて」です!
お楽しみに!


📣 ウェルスナビは一緒に働く仲間を募集しています 📣

https://hrmos.co/pages/wealthnavi/jobs?category=1243739934161813504

著者プロフィール

高原 勇輝(たかはら ゆうき)

2023 年 2 月にウェルスナビに、フルスタックエンジニアとして入社。モバイルアプリの開発が得意ですが、バックエンド開発にも興味を持っています。趣味は LeetCode の問題解決、キャンプ、そしてサッカー観戦です。