无法使用 Dropwizard 测试安全资源

Unable to test secure resources with Dropwizard

我正在尝试为我的 Dropwizard 应用程序中的安全资源编写测试(我使用的是 Dropwizard 0.8.1)。当我 运行 应用程序时授权工作正常,但我似乎无法编写可以工作的测试。该代码主要基于示例 Hello World 应用程序,尽管传递了正确的授权 headers,但我仍然收到 401。我有以下内容:

HelloWorldResource

package com.example.helloworld.resources;

import com.example.helloworld.core.Saying;
import com.example.helloworld.core.User;
import com.google.common.base.Optional;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.concurrent.atomic.AtomicLong;

@Path("/hello-world")
@Produces(MediaType.APPLICATION_JSON)
public class HelloWorldResource {
    private final String template;
    private final String defaultName;
    private final AtomicLong counter;

    public HelloWorldResource(String template, String defaultName) {
        this.template = template;
        this.defaultName = defaultName;
        this.counter = new AtomicLong();
    }

    @GET
    @Timed
    public Saying sayHello(@QueryParam("name") Optional<String> name) {
        final String value = String.format(template, name.or(defaultName));
        return new Saying(counter.incrementAndGet(), value);
    }

    @GET
    @Path("secure")
    @Timed
    public Saying sayHelloSecurely(@Auth User user) {
        final String value = String.format(template, user.getName());
        return new Saying(counter.incrementAndGet(), value);
    }
}

package com.example.helloworld.core;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.Length;

public class Saying {
    private final long id;

    @Length(max = 3)
    private final String content;

    @JsonCreator
    public Saying(@JsonProperty long id, @JsonProperty String content) {
        this.id = id;
        this.content = content;
    }

    @JsonProperty
    public long getId() {
        return id;
    }

    @JsonProperty
    public String getContent() {
        return content;
    }
}

验证者

package com.example.helloworld.auth;


import com.example.helloworld.core.User;
import com.google.common.base.Optional;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;

public class SimpleAuthenticator implements Authenticator<BasicCredentials, User> {
    public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException {
        if ("secret".equals(credentials.getPassword())) {
            return Optional.of(new User(credentials.getUsername()));
        }
        return Optional.absent();
    }
}

安全测试

package com.example.helloworld.resources;

import com.example.helloworld.auth.SimpleAuthenticator;
import com.example.helloworld.core.Saying;
import com.example.helloworld.core.User;
import io.dropwizard.auth.AuthFactory;
import io.dropwizard.auth.basic.BasicAuthFactory;
import io.dropwizard.testing.junit.ResourceTestRule;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.ClassRule;
import org.junit.Test;

import javax.ws.rs.core.HttpHeaders;

public class SecureHelloWorldResourceTest {
    @ClassRule
    public static final ResourceTestRule resources =
            ResourceTestRule.builder()
                    .addProvider(RolesAllowedDynamicFeature.class)
                    .addProvider(AuthFactory.binder(new BasicAuthFactory<User>(new SimpleAuthenticator(),
                            "Authentication Realm",
                            User.class)))
                    .addResource(new HelloWorldResource("Hey There %s", "DefaultName"))
                    .build();

    @Test
    public void testGetPerson() {
        resources
                .client()
                .target("/hello-world/secure")
                .request()
                .header(HttpHeaders.AUTHORIZATION, "Basic Z29vZC1ndXk6c2VjcmV0").get(Saying.class); //Decodes to "good-guy:secret"

    }
}

更新

阅读 https://github.com/dropwizard/dropwizard/issues/922 后,我开始使用 Grizzly 容器,现在我收到一个与 Saying 序列化相关的新错误:

堆栈跟踪

javax.ws.rs.ProcessingException: Error reading entity from input stream.
    at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:868)
    at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:785)
    at org.glassfish.jersey.client.ClientResponse.readEntity(ClientResponse.java:326)
    at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:790)
    at org.glassfish.jersey.client.JerseyInvocation.access0(JerseyInvocation.java:91)
    at org.glassfish.jersey.client.JerseyInvocation.call(JerseyInvocation.java:687)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:444)
    at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:683)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:411)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:307)
    at com.example.helloworld.resources.SecureHelloWorldResourceTest.testGetPerson(SecureHelloWorldResourceTest.java:35)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access[=14=]0(ParentRunner.java:58)
    at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
    at io.dropwizard.testing.junit.ResourceTestRule.evaluate(ResourceTestRule.java:199)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Argument #0 of constructor [constructor for com.example.helloworld.core.Saying, annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}] has no property name annotation; must have name when multiple-parameter constructor annotated as Creator
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:267)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:242)
    at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:143)
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:439)
    at com.fasterxml.jackson.databind.ObjectReader._findRootDeserializer(ObjectReader.java:1564)
    at com.fasterxml.jackson.databind.ObjectReader._bind(ObjectReader.java:1403)
    at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:858)
    at com.fasterxml.jackson.jaxrs.base.ProviderBase.readFrom(ProviderBase.java:808)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.invokeReadFrom(ReaderInterceptorExecutor.java:266)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.aroundReadFrom(ReaderInterceptorExecutor.java:236)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:156)
    at org.glassfish.jersey.message.internal.MessageBodyFactory.readFrom(MessageBodyFactory.java:1085)
    at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:853)
    ... 41 more
Caused by: java.lang.IllegalArgumentException: Argument #0 of constructor [constructor for com.example.helloworld.core.Saying, annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}] has no property name annotation; must have name when multiple-parameter constructor annotated as Creator
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addDeserializerConstructors(BasicDeserializerFactory.java:508)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:325)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:258)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:216)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:143)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:405)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:354)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:262)
    ... 53 more


Process finished with exit code -1

安全测试

package com.example.helloworld.resources;

import com.example.helloworld.auth.SimpleAuthenticator;
import com.example.helloworld.core.Saying;
import com.example.helloworld.core.User;
import io.dropwizard.auth.AuthFactory;
import io.dropwizard.auth.basic.BasicAuthFactory;
import io.dropwizard.testing.junit.ResourceTestRule;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.ClassRule;
import org.junit.Test;

import javax.ws.rs.core.HttpHeaders;

public class SecureHelloWorldResourceTest {
    @ClassRule
    public static final ResourceTestRule resources =
            ResourceTestRule.builder()
                    .addProvider(RolesAllowedDynamicFeature.class)
                    .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
                    .addProvider(AuthFactory.binder(new BasicAuthFactory<User>(new SimpleAuthenticator(),
                            "Authentication Realm",
                            User.class)))
                    .addResource(new HelloWorldResource("Hey There %s", "DefaultName"))
                    .build();

    @Test
    public void testGetPerson() {
        resources
                .getJerseyTest()
                //.client()
                .target("/hello-world/secure")
                .request()
                    .header(HttpHeaders.AUTHORIZATION, "Basic Z29vZC1ndXk6c2VjcmV0").get(Saying.class);

    }
}

我通过使用名为 JsonProperties 的构造函数注释参数来解决更新中的序列化错误,这似乎有点多余。

public class Saying {
    private final long id;

    @Length(max = 3)
    private final String content;

    @JsonCreator
    public Saying(@JsonProperty("id") long id, @JsonProperty("content") String content) {//Note the addition of the ("id") and ("content") 
        this.id = id;
        this.content = content;
    }

    @JsonProperty
    public long getId() {
        return id;
    }

    @JsonProperty
    public String getContent() {
        return content;
    }
}

测试现在看起来像这样:

package com.example.helloworld.resources;

import com.example.helloworld.auth.SimpleAuthenticator;
import com.example.helloworld.core.Saying;
import com.example.helloworld.core.User;
import io.dropwizard.auth.AuthFactory;
import io.dropwizard.auth.basic.BasicAuthFactory;
import io.dropwizard.testing.junit.ResourceTestRule;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.HttpHeaders;

public class SecureHelloWorldResourceTest {
    @Rule
    public ExpectedException exception = ExpectedException.none();
    @ClassRule
    public static final ResourceTestRule resources =
            ResourceTestRule.builder()
                    .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
                    .addProvider(AuthFactory.binder(new BasicAuthFactory<User>(new SimpleAuthenticator(),
                            "Authentication Realm",
                            User.class)))
                    .addResource(new HelloWorldResource("Hey There %s", "DefaultName"))
                    .build();

    @Test
    public void testAuthorizedRequest() {
        Saying result = resources
                .getJerseyTest()
                .target("/hello-world/secure")
                .request()
                .header(HttpHeaders.AUTHORIZATION, "Basic Z29vZC1ndXk6c2VjcmV0").get(Saying.class);//good-guy:secret

        Assert.assertEquals("Hey There good-guy", result.getContent());
    }

    @Test
    public void testDeniedRequest() {
        exception.expect(NotAuthorizedException.class);
        resources
                .getJerseyTest()
                .target("/hello-world/secure")
                .request()
                .header(HttpHeaders.AUTHORIZATION, "Basic YmFkLWd1eTpwYXNzd29yZA==").get(Saying.class);//bad-guy:password


    }
}