简体   繁体   English

给定一个Ratpack RequestFixture测试,我如何让灯具调用Request#beforeSend?

[英]Given a Ratpack RequestFixture test, how can I have the fixture invoke Request#beforeSend?

This question is in the context of a Ratpack RequestFixture Spock test, for a Ratpack chain authenticating with RatpackPac4j#requireAuth , and employing a workaround for the missing WWW-Authenticate header (as described in the answer to this question ) 这个问题是在RatpackPac4j#requireAuth RequestFixture Spock测试的背景下进行的,用于使用RatpackPac4j#requireAuth身份验证的RatpackPac4j#requireAuth链,并针对丢失的WWW-Authenticate标头采用了变通方法(如对该问题的回答中所述)

The problem I have is, I find that #beforeSend appears to be uncalled when the response is obtained from GroovyRequestFixture#handle (a wrapper for RequestFixture#handle ). #beforeSend的问题是,当从GroovyRequestFixture#handleRequestFixture#handle的包装器)获得响应时,我发现#beforeSend似乎GroovyRequestFixture#handle The work-around depends on this to work, so I can't test it. 解决方法取决于此方法,因此我无法对其进行测试。 Is there a way to get #beforeSend called on the response represented by the HandlingResult returned? 有没有办法在返回的HandlingResult表示的响应上调用#beforeSend

For example, this test case fails with the assertion that the WWW-Authenticate header is present, even though the code this is adapted from inserts the header correctly when called in the actual application. 例如,即使在实际应用程序中调用该方法所适用的代码正确插入了该头之后,该测试用例仍会断言存在WWW-Authenticate头。 The chain under test is testChain , skip to the end for the failing assertion: 被测试的链是testChain ,跳到失败断言的末尾:

package whatever

import com.google.inject.Module
import groovy.transform.Canonical
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.pac4j.core.profile.jwt.JwtClaims
import org.pac4j.http.client.direct.HeaderClient
import org.pac4j.jwt.config.encryption.EncryptionConfiguration
import org.pac4j.jwt.config.encryption.SecretEncryptionConfiguration
import org.pac4j.jwt.config.signature.SecretSignatureConfiguration
import org.pac4j.jwt.config.signature.SignatureConfiguration
import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator
import org.pac4j.jwt.profile.JwtGenerator
import org.pac4j.jwt.profile.JwtProfile
import ratpack.groovy.handling.GroovyChainAction
import ratpack.groovy.test.handling.GroovyRequestFixture
import ratpack.guice.Guice
import ratpack.http.Response
import ratpack.http.Status
import ratpack.jackson.Jackson
import ratpack.pac4j.RatpackPac4j
import ratpack.registry.Registry
import ratpack.session.SessionModule
import ratpack.test.handling.HandlerExceptionNotThrownException
import ratpack.test.handling.HandlingResult
import spock.lang.Specification

@CompileStatic
class AuthenticatorTest extends Specification {
    static byte[] salt = new byte[32] // dummy salt

    static SignatureConfiguration signatureConfiguration = new SecretSignatureConfiguration(salt)
    static EncryptionConfiguration encryptionConfiguration = new SecretEncryptionConfiguration(salt)
    static JwtAuthenticator authenticator = new JwtAuthenticator(signatureConfiguration, encryptionConfiguration)
    static JwtGenerator generator = new JwtGenerator(signatureConfiguration, encryptionConfiguration)
    static HeaderClient headerClient = new HeaderClient("Authorization", "bearer ", authenticator)

    /** A stripped down user class */
    @Canonical
    static class User {
        final String id
    }

    /** A stripped down user registry class */
    @Canonical
    static class UserRegistry {
        private final Map<String, String> users = [
            'joebloggs': 'sekret'
        ]

        User authenticate(String id, String password) {
            if (password != null && users[id] == password)
               return new User(id)
            return null
        }
    }

    /** Generates a JWT token for a given user
     *
     * @param userId - the name of the user
     * @return A JWT token encoded as a string
     */
    static String generateToken(String userId) {
        JwtProfile profile = new JwtProfile()
        profile.id = userId
        profile.addAttribute(JwtClaims.ISSUED_AT, new Date())
        String token = generator.generate(profile)
        token
    }

    static void trapExceptions(HandlingResult result) {
        try {
            Throwable t = result.exception(Throwable)
            throw t
        }
        catch (HandlerExceptionNotThrownException ignored) {
        }
    }

    /** Composes a new registry binding the module class passed
     * as per SO question https://stackoverflow.com/questions/50814817/how-do-i-mock-a-session-in-ratpack-with-requestfixture
     */
    static Registry addModule(Registry registry, Class<? extends Module> module) {
        Guice.registry { it.module(module) }.apply(registry)
    }

    GroovyChainAction testChain = new GroovyChainAction() {
        @Override
        @CompileDynamic
        void execute() throws Exception {

            register addModule(registry, SessionModule)

            all RatpackPac4j.authenticator(headerClient)

            all {
                /*
                 * This is a workaround for an issue in RatpackPac4j v2.0.0, which doesn't
                 * add the WWW-Authenticate header by itself.
                 *
                 * See https://github.com/pac4j/ratpack-pac4j/issues/3
                 *
                 * This handler needs to be ahead of any potential causes of 401 statuses
                 */
                response.beforeSend { Response response ->
                    if (response.status.code == 401) {
                        response.headers.set('WWW-Authenticate', 'bearer realm="authenticated api"')
                    }
                }
                next()
            }

            post('login') { UserRegistry users ->
                parse(Jackson.fromJson(Map)).then { Map data ->
                    // Validate the credentials
                    String id = data.user
                    String password = data.password
                    User user = users.authenticate(id, password)
                    if (user == null) {
                        clientError(401) // Bad authentication credentials
                    } else {
                        response.contentType('text/plain')

                        // Authenticates ok. Issue a JWT token to the client which embeds (signed, encrypted)
                        // certain standardised metadata of our choice that the JWT validation will use.
                        String token = generateToken(user.id)
                        render token
                    }
                }
            }

            get('unprotected') {
                render "hello"
            }

            // All subsequent paths require authentication
            all RatpackPac4j.requireAuth(HeaderClient)

            get('protected') {
                render "hello"
            }

            notFound()
        }
    }

    @CompileDynamic
    def "should be denied protected path, unauthorised..."() {
        given:
        def result = GroovyRequestFixture.handle(testChain) {
            uri 'protected'
            method 'GET'
        }

        expect:
        result.status == Status.of(401) // Unauthorized


        // THIS FAILS BECAUSE Response#beforeSend WASN'T INVOKED BY GroovyRequestFixture
        result.headers['WWW-Authenticate'] == 'bearer realm="authenticated api"'

        // If the server threw, rethrow that
        trapExceptions(result)
    }
}

Best answer so far... or more strictly, a workaround to sidestep the limitations of RequestFixture , is: don't use RequestFixture . 到目前为止最好的答案...或更严格地说,一种规避RequestFixture局限性的解决方法是:不要使用RequestFixture Use GroovyEmbeddedApp 使用GroovyEmbeddedApp

(Credit to Dan Hyun on the Ratpack slack channel) (在Ratpack松弛频道上提供给Dan Hyun

RequestFixture is only meant to check handler behavior, it doesn't do a lot of things - it won't serialize responses. RequestFixture仅用于检查处理程序行为,它不会做很多事情-不会序列化响应。 EmbeddedApp is probably the way to go for most testing. EmbeddedApp可能是大多数测试的方法。 You'd care more about overall interaction rather than how an individual handler does a thing, unless it was a highly reused component or is middleware that is used by other apps 您将更关心整体交互,而不是单个处理程序如何做某事,除非它是高度复用的组件或其他应用程序使用的中间件

An modified version of the example above follows, I've marked the modified sections in the comments: 上面示例的修改版本如下,我在注释中标记了修改后的部分:

package whatever

import com.google.inject.Module
import groovy.transform.Canonical
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.pac4j.core.profile.jwt.JwtClaims
import org.pac4j.http.client.direct.HeaderClient
import org.pac4j.jwt.config.encryption.EncryptionConfiguration
import org.pac4j.jwt.config.encryption.SecretEncryptionConfiguration
import org.pac4j.jwt.config.signature.SecretSignatureConfiguration
import org.pac4j.jwt.config.signature.SignatureConfiguration
import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator
import org.pac4j.jwt.profile.JwtGenerator
import org.pac4j.jwt.profile.JwtProfile
import ratpack.groovy.test.embed.GroovyEmbeddedApp
import ratpack.guice.Guice
import ratpack.http.Response
import ratpack.http.Status
import ratpack.http.client.ReceivedResponse
import ratpack.jackson.Jackson
import ratpack.pac4j.RatpackPac4j
import ratpack.registry.Registry
import ratpack.session.SessionModule
import ratpack.test.handling.HandlerExceptionNotThrownException
import ratpack.test.handling.HandlingResult
import ratpack.test.http.TestHttpClient
import spock.lang.Specification

@CompileStatic
class TempTest extends Specification {
    static byte[] salt = new byte[32] // dummy salt

    static SignatureConfiguration signatureConfiguration = new SecretSignatureConfiguration(salt)
    static EncryptionConfiguration encryptionConfiguration = new SecretEncryptionConfiguration(salt)
    static JwtAuthenticator authenticator = new JwtAuthenticator(signatureConfiguration, encryptionConfiguration)
    static JwtGenerator generator = new JwtGenerator(signatureConfiguration, encryptionConfiguration)
    static HeaderClient headerClient = new HeaderClient("Authorization", "bearer ", authenticator)

    /** A stripped down user class */
    @Canonical
    static class User {
        final String id
    }

    /** A stripped down user registry class */
    @Canonical
    static class UserRegistry {
        private final Map<String, String> users = [
            'joebloggs': 'sekret'
        ]

        User authenticate(String id, String password) {
            if (password != null && users[id] == password)
                return new User(id)
            return null
        }
    }

    /** Generates a JWT token for a given user
     *
     * @param userId - the name of the user
     * @return A JWT token encoded as a string
     */
    static String generateToken(String userId) {
        JwtProfile profile = new JwtProfile()
        profile.id = userId
        profile.addAttribute(JwtClaims.ISSUED_AT, new Date())
        String token = generator.generate(profile)
        token
    }

    static void trapExceptions(HandlingResult result) {
        try {
            Throwable t = result.exception(Throwable)
            throw t
        }
        catch (HandlerExceptionNotThrownException ignored) {
        }
    }

    /** Composes a new registry binding the module class passed
     * as per SO question https://stackoverflow.com/questions/50814817/how-do-i-mock-a-session-in-ratpack-with-requestfixture
     */
    static Registry addModule(Registry registry, Class<? extends Module> module) {
        Guice.registry { it.module(module) }.apply(registry)
    }

    /*** USE GroovyEmbeddedApp HERE INSTEAD OF GroovyResponseFixture ***/
    GroovyEmbeddedApp testApp = GroovyEmbeddedApp.ratpack {
        bindings {
            module SessionModule
        }

        handlers {
            all RatpackPac4j.authenticator(headerClient)

            all {
                /*
                 * This is a workaround for an issue in RatpackPac4j v2.0.0, which doesn't
                 * add the WWW-Authenticate header by itself.
                 *
                 * See https://github.com/pac4j/ratpack-pac4j/issues/3
                 *
                 * This handler needs to be ahead of any potential causes of 401 statuses
                 */
                response.beforeSend { Response response ->
                    if (response.status.code == 401) {
                        response.headers.set('WWW-Authenticate', 'bearer realm="authenticated api"')
                    }
                }
                next()
            }

            post('login') { UserRegistry users ->
                parse(Jackson.fromJson(Map)).then { Map data ->
                    // Validate the credentials
                    String id = data.user
                    String password = data.password
                    User user = users.authenticate(id, password)
                    if (user == null) {
                        clientError(401) // Bad authentication credentials
                    } else {
                        response.contentType('text/plain')

                        // Authenticates ok. Issue a JWT token to the client which embeds (signed, encrypted)
                        // certain standardised metadata of our choice that the JWT validation will use.
                        String token = generateToken(user.id)
                        render token
                    }
                }
            }

            get('unprotected') {
                render "hello"
            }

            // All subsequent paths require authentication
            all RatpackPac4j.requireAuth(HeaderClient)

            get('protected') {
                render "hello"
            }

            notFound()
        }
    }


    /*** THIS NOW ALTERED TO USE testApp ***/
    @CompileDynamic
    def "should be denied protected path, unauthorised..."() {
        given:
        TestHttpClient client = testApp.httpClient
        ReceivedResponse response = client.get('protected')

        expect:
        response.status == Status.of(401) // Unauthorized
        response.headers['WWW-Authenticate'] == 'bearer realm="authenticated api"'
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 FitNessse测试找不到夹具 - FitNessse Test can not find Fixture 如何测试调用父类受保护(不需要的)方法的方法? - How can I test a method which invoke protected (unwanted) methods of parent class? 如何在运行时类路径上具有Maven依赖性而不是测试类路径? - How can I have a Maven dependency on the runtime classpath but not the test classpath? 如何在泛型类型的集合上调用JUnit参数化测试运行器? - How can I invoke a JUnit Parametrized test runner on a collection of generic types? 如何将具有对象作为参数的测试方法单元化 - How can I unit test methods that have object as parameters 如何测试具有多个返回的函数? - How can I test functions that have multiple returns? 如何手动调用SimpleWebServiceInboundGateway? - How can I invoke SimpleWebServiceInboundGateway manually? 是否可以对@BeforeEach 进行参数化,即根据每个@Test 给出的参数调用不同的@BeforeEach? - Is it possible to parametrize a @BeforeEach, i.e. invoke a different @BeforeEach depending on a parameter given by each @Test? 我如何单元测试java中类的特定方法在给定的持续时间内被定期调用 - How can I Unit test that a specific method of a class in java was periodically invoked for a given duration 我可以使用 MockRestServiceServer 测试 HttpClient 请求吗 - Can I test HttpClient Request With MockRestServiceServer
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM