Keycloak認証を使うリソースサーバーのGroovy Spockテスト

概要

Keycloakによる認証を使う、リソースサーバー(Spring Bootクライアント)でのGroovy Spockテスト実装メモ。

この手順で使用したコードは、以下に公開しているので、こちらも参考にしてください。
https://github.com/yo1000/kc-resource/ac9914ae02#try-testing-with-only-kc-resource-server

また、テストについては、既に過去のポストで触れているため、ここでは、Groovy Spockを適用するにあたって、変更が必要となる部分について書いていきます。

要件

環境

今回の作業環境は以下のとおりです。

  • Java 1.8.0_131
  • Spring Boot 1.5.9.RELEASE
  • Keycloak 3.4.1.Final
  • Groovy 2.4.11
  • Spock 1.1-groovy-2.4
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.12.5
BuildVersion:	16F2073

$ java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)

pom.xmlの変更

依存関係の追加

以下の依存を追加します。

<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-all</artifactId>
  <version>2.4.11</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-core</artifactId>
  <version>1.1-groovy-2.4</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-spring</artifactId>
  <version>1.1-groovy-2.4</version>
  <scope>test</scope>
</dependency>

ビルド構成の変更

以下のようにビルドプラグインを追加します。

<plugin>
  <groupId>org.codehaus.gmavenplus</groupId>
  <artifactId>gmavenplus-plugin</artifactId>
  <version>1.5</version>
  <executions>
    <execution>
      <goals>
        <goal>addTestSources</goal>
        <goal>testCompile</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <testSources>
      <fileset>
        <directory>${pom.basedir}/src/test/groovy</directory>
        <includes>
          <include>**/*.groovy</include>
        </includes>
      </fileset>
    </testSources>
  </configuration>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.19.1</version>
  <configuration>
    <includes>
      <include>**/*Test.java</include>
      <include>**/*Tests.java</include>
      <include>**/*Spec.java</include>
      <include>**/*Specs.java</include>
    </includes>
  </configuration>
</plugin>

テストの実装

テストコード

過去のポストで書いたテストと、内容は同じものです。Groovy Spock用に書き改めています。

package com.yo1000.keycloak.resource.server

import org.junit.Before
import org.keycloak.KeycloakPrincipal
import org.keycloak.adapters.RefreshableKeycloakSecurityContext
import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll

@Unroll
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
class KcResourceServerControllerSpecs extends Specification {
    @Autowired
    WebApplicationContext context
    @Shared
    MockMvc mockMvc

    @Before
    def beforeTestEach() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build()
    }

    /**
     * When the user has Admin and User roles,
     * then can access endpoints that require Admin role or User role.
     */
    def when_the_user_has_Admin_and_User_roles_then_can_access_endpoints_that_require_Admin_role_or_User_role() {
        given:
        def token = new KeycloakAuthenticationToken(
                new SimpleKeycloakAccount(
                        Mockito.mock(KeycloakPrincipal.class),
                        ["admin", "user"].toSet(),
                        Mockito.mock(RefreshableKeycloakSecurityContext.class)),
                false)

        expect:
        mockMvc.perform(MockMvcRequestBuilders.get("/kc/resource/server/admin")
                .with(SecurityMockMvcRequestPostProcessors.authentication(token)))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())

        mockMvc.perform(MockMvcRequestBuilders.get("/kc/resource/server/user")
                .with(SecurityMockMvcRequestPostProcessors.authentication(token)))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
    }

    /**
     * When the user has only User role,
     * then can access endpoints that require User role,
     * but can't access endpoints that require Admin role.
     */
    def when_the_user_has_only_User_role_then_can_access_endpoints_that_require_User_role_but_cant_access_endpoints_that_require_Admin_role() {
        given:
        def token = new KeycloakAuthenticationToken(
                new SimpleKeycloakAccount(
                        Mockito.mock(KeycloakPrincipal.class),
                        ["user"].toSet(),
                        Mockito.mock(RefreshableKeycloakSecurityContext.class)),
                false)

        expect:
        mockMvc.perform(MockMvcRequestBuilders.get("/kc/resource/server/admin")
                .with(SecurityMockMvcRequestPostProcessors.authentication(token)))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isForbidden())

        mockMvc.perform(MockMvcRequestBuilders.get("/kc/resource/server/user")
                .with(SecurityMockMvcRequestPostProcessors.authentication(token)))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
    }
}

デモ

参考までに実際に動かした結果の一部を、以下に残しておきます。

$ ./mvnw clean test

..

2018-01-04 02:34:09.638  INFO 74374 --- [           main] .y.k.r.s.KcResourceServerControllerSpecs : Started KcResourceServerControllerSpecs in 3.334 seconds (JVM running for 13.218)
2018-01-04 02:34:09.685  INFO 74374 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet ''
2018-01-04 02:34:09.685  INFO 74374 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : FrameworkServlet '': initialization started
2018-01-04 02:34:09.698  INFO 74374 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : FrameworkServlet '': initialization completed in 12 ms

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /kc/resource/server/admin
       Parameters = {}
          Headers = {}

Handler:
             Type = com.yo1000.keycloak.resource.server.KcResourceServerController
           Method = public java.lang.String com.yo1000.keycloak.resource.server.KcResourceServerController.getAdminResource()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], Content-Type=[text/plain;charset=UTF-8], Content-Length=[16]}
     Content type = text/plain;charset=UTF-8
             Body = ADMIN Resource!!
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

..
avatar

Written by yo1000 | YO!CHI KIKUCHI
Loves 🌱 Spring, 🦢 Pelikan fountain pen and 🦁 FINAL FANTASY VIII