如何使用 deadbolt2 DeadboltActions 测试控制器,或者是否有另一个框架可以轻松实现?

How to test controllers using deadbolt2 DeadboltActions or is there another framework which allows this easily?

我正在使用 Play! 2.4 用Deadbolt2 进行授权。但是,由于我引入了授权规则,我无法为我的控制器编写成功的测试。例如:

class VisitController @Inject() (authorization: DeadboltActions) extends Controller {
  def fetchDailyVisits(date: Date) = authorization.Restrict(List(Array(ADMIN_ROLE), Array(MANAGER_ROLE))) {
    Action.async {
      visitService.findDailyVisits(date).map(result =>
        Ok(Json.toJson(result))
      )
    }
  }
}

我在测试中使用 specs2。我的测试看起来像这样 atm:

class VisitControllerSpec extends PlaySpecification with Mockito with ScalaFutures {
  val deadboltActions = mock[DeadboltActions]
"VisitControllerSpec#fetchDailyVisits" should {

    val testDate = Date.from(LocalDate.of(2016, 2, 25)
      .atStartOfDay(ZoneId.systemDefault()).toInstant)

    "Return Status Ok with returned list" in {

      val expected = List(completeVisitWithId, anotherCompleteVisitWithId)
      visitService.findDailyVisits(testDate) returns Future { expected }

      val request = FakeRequest(GET, "/visits?date=2016-02-25")

      val result = new VisitController(deadboltActions)
        .fetchDailyVisits(testDate)(request)

      result.futureValue.header.status must beEqualTo(OK)
      contentAsJson(result) must_== Json.toJson(expected)
    }
  }
}

如何以指定允许用户访问的方式模拟 deadboltActions?

还有别的办法吗?也许通过提供不同的 DeadboltHandler?看起来很明显这将是要走的路,我只是似乎无法弄清楚并且那里没有很多 Deadbolt2 示例(至少对于 scala)。

或者,更极端的是,是否有任何其他授权框架可以很好地与 Scala Play 配合使用,并允许将安全性作为一个横切关注点来处理,而不会污染控制器? Deadbolt2 由于这个原因太有限了,但老实说我找不到更好的授权框架(除非我自己写)。

您可以通过多种不同的方式执行此操作。

如果您的 DeadboltHandler 注入了用于访问主题的 DAO,您可以覆盖 DAO 的绑定以提供包含测试主题的绑定。

abstract class AbstractControllerSpec extends PlaySpecification {
  sequential
  isolated

  def testApp: Application =  new  GuiceApplicationBuilder().in(Mode.Test).bindings(bind[SubjectDao].to[TestSubjectDao]).build()
}

有关使用此方法的示例,请参阅 the test app

或者,您可以扩展 DeadboltHandler 实现以覆盖 getSubject 并从此处提供测试对象。绑定的处理方式同上。

最后,您可以保留所有代码 as-is 并使用主题填充测试数据库;您发送的请求将由您的身份验证要求(headers、cookie 中的内容等)决定。

对于单元测试,类似的事情也适用。鉴于 SubjectDao 有一些 hard-coded 科目用于测试目的,您可以使用 WithApplication 和注射器 look-up 来获得您需要的东西。

class TestSubjectDao extends SubjectDao {

  val subjects: Map[String, Subject] = Map("greet" -> new SecuritySubject("greet",
                                                                           List(SecurityRole("foo"),
                                                                                 SecurityRole("bar")),
                                                                           List(SecurityPermission("killer.undead.zombie"))),
                                            "lotte" -> new SecuritySubject("lotte",
                                                                            List(SecurityRole("hurdy")),
                                                                            List(SecurityPermission("killer.undead.vampire"))),
                                            "steve" -> new SecuritySubject("steve",
                                                                            List(SecurityRole("bar")),
                                                                            List(SecurityPermission("curator.museum.insects"))),
                                            "mani" -> new SecuritySubject("mani",
                                                                           List(SecurityRole("bar"),
                                                                                 SecurityRole("hurdy")),
                                                                           List(SecurityPermission("zombie.movie.enthusiast"))),
                                            "trippel" -> new SecuritySubject("trippel",
                                                                           List(SecurityRole("foo"),
                                                                                 SecurityRole("hurdy")),
                                                                           List[SecurityPermission]()))

  override def user(userName: String): Option[Subject] = subjects.get(userName)
}

控制器看起来像这样:

class Subject @Inject()(deadbolt: DeadboltActions) extends Controller {

  def subjectMustBePresent = deadbolt.SubjectPresent()() { authRequest =>
    Future {
      Ok("Content accessible")
    }
  }
}

然后我们可以像这样对其进行单元测试:

import be.objectify.deadbolt.scala.DeadboltActions
import be.objectify.deadbolt.scala.test.controllers.composed.Subject
import be.objectify.deadbolt.scala.test.dao.{SubjectDao, TestSubjectDao}
import play.api.Mode
import play.api.inject._
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.mvc.{Result, Results}
import play.api.test.{FakeRequest, PlaySpecification, WithApplication}

import scala.concurrent.Future

object SubjectPresentUnitSpec extends PlaySpecification with Results {
  "Subject present " should {
    "should result in a 401 when no subject is present" in new WithApplication(new GuiceApplicationBuilder().in(Mode.Test).bindings(bind[SubjectDao].to[TestSubjectDao]).build()) {
      val deadbolt: DeadboltActions = implicitApp.injector.instanceOf[DeadboltActions]
      val controller = new Subject(deadbolt)
      val result: Future[Result] = call(controller.subjectMustBePresent(), FakeRequest())
      val statusCode: Int = status(result)
      statusCode must be equalTo 401
    }

    "should result in a 200 when a subject is present" in new WithApplication(new GuiceApplicationBuilder().in(Mode.Test).bindings(bind[SubjectDao].to[TestSubjectDao]).build()) {
      val deadbolt: DeadboltActions = implicitApp.injector.instanceOf[DeadboltActions]
      val controller = new Subject(deadbolt)
      val result: Future[Result] = call(controller.subjectMustBePresent(), FakeRequest().withHeaders(("x-deadbolt-test-user", "greet")))
      val statusCode: Int = status(result)
      statusCode must be equalTo 200
    }
  }
}  

它没有完全回答我最初的问题,这主要与 Deadbolt2 有关,但我一直对我必须在我的控制器中指定我的授权规则这一事实感到沮丧,这不是真正的交叉切入。

Steve Chaloner提供的答案有帮助,但仍然让我经历了一些困难。

输入Panoptes。此授权框架基于过滤器而不是操作链接,因此它允许在中央位置和控制器外部轻松指定授权规则。

Panoptes 中设置您的安全规则有点类似于 Spring 安全,它看起来像这样:

class BasicAuthHandler extends AuthorizationHandler {

  override def config: Set[(Pattern, _ <: AuthorizationRule)] = {
    Set(
      Pattern(Some(POST), "/products") -> atLeastOne(withRole("Admin"), withRole("Manager"))
      Pattern(Some(GET), "/cart[/A-Za-z0-9]*") -> withRole("Admin"),
      Pattern(None, "/orders[/A-Za-z0-9]*") -> withRole("Admin")
    )
  }
}

除此之外,您需要几行代码来声明过滤器并插入您的 AuthorizationHandler。

class Filters @Inject()(securityFilter: SecurityFilter) extends HttpFilters {
  override def filters = Seq(securityFilter)
}

class ControllerProviderModule extends AbstractModule {
  override def configure(): Unit = {   bind(classOf[AuthorizationHandler]).to(classOf[MyAuthorizationHandler])
  }
}

git 存储库中的自述文件包含更多详细信息和代码示例。

它也可以自定义到允许创建您自己的 AuthorizationRules 的程度。在我的项目中,我有一个要求,我需要检查拨打电话的移动设备是否已在系统中注册。我可以编写一个 AuthorizationRule 来为我处理每个路径与我的模式匹配的请求。

单元测试控制器非常简单,因为可以省略对安全层的任何模拟。它们可以像其他任何东西一样进行测试 class.

如果您遇到类似问题或者认为授权规则不属于控制器,请尝试 Panoptes,它可能适合您的需要。希望这对其他人有帮助。