Как протестировать контроллеры с помощью deadbolt2 DeadboltActions или есть другая структура, которая позволяет это легко?

Я использую 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 в тестах. Мой тест выглядит так:

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 слишком ограничен, но я, честно говоря, не могу найти лучшую структуру авторизации (если я не напишу свою).

2 ответа

Решение

Он не дает точного ответа на мой первоначальный вопрос, который в основном был связан с Deadbolt2, но я продолжал расстраиваться из-за того, что мне приходилось указывать правила авторизации в моих контроллерах, что на самом деле не является сквозной задачей.

Ответ, предоставленный Steve Chaloner, помогает, но все же заставил меня пройти через несколько обручей.

Введите Panoptes. Эта структура авторизации основана на фильтрах, а не на цепочке действий, поэтому она позволяет легко определять правила авторизации в центральном местоположении и за пределами контроллеров.

Установка ваших правил безопасности в Panoptes чем-то похож на Spring Security и выглядит так:

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])
  }
}

Файл README в репозитории git содержит больше деталей и примеров кода.

Это также настраивается так, что позволяет создавать ваши собственные AuthorizationRules. В моем проекте у меня есть требование, чтобы мне нужно было проверить, зарегистрировано ли мобильное устройство, зарегистрированное в системе. Я могу написать AuthorizationRule для обработки этого для меня для каждого запроса, путь которого совпадает с моим шаблоном.

Модульное тестирование контроллеров очень простое, потому что любое моделирование уровня безопасности может быть опущено. Они могут быть проверены как любой другой класс.

Если у вас возникают похожие проблемы или вы считаете, что правила авторизации не относятся к контроллерам, попробуйте Panoptes, это может удовлетворить ваши потребности. Надеюсь, это поможет кому-то еще.

Есть несколько разных способов сделать это.

Если твой DeadboltHandler если для доступа к объекту введен DAO, вы можете переопределить привязку DAO, чтобы предоставить объект, содержащий испытуемых.

abstract class AbstractControllerSpec extends PlaySpecification {
  sequential
  isolated

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

Смотрите тестовое приложение для примера использования этого подхода.

Кроме того, вы можете продлить DeadboltHandler реализация переопределить getSubject и предоставить испытуемый отсюда. Привязка обрабатывается так же, как указано выше.

Наконец, вы можете оставить весь свой код как есть и заполнить тестовую базу данных субъектами; отправленные вами запросы будут зависеть от ваших требований к аутентификации (заголовки, файлы cookie и т. д.).

Для модульного тестирования применяется нечто подобное. Учитывая SubjectDao который имеет несколько жестко закодированных предметов для целей тестирования, вы можете использовать WithApplication и поиск инжектора, чтобы получить то, что вам нужно.

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
    }
  }
}  
Другие вопросы по тегам