diff --git a/.gitignore b/.gitignore index ec4ccaaf..3fc8d32c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,12 @@ dist /.target /.cache *.DS_Store -.bsp/ + +# Metals +.metals/* +.bloop/* +.vscode/* +**/.bloop/* +**/metals.sbt + +.bsp/* diff --git a/build.sbt b/build.sbt index 39c205ac..7d191a36 100644 --- a/build.sbt +++ b/build.sbt @@ -3,8 +3,10 @@ import sbt.CrossVersion lazy val repo: String = "https://s01.oss.sonatype.org" lazy val scala213: String = "2.13.12" -lazy val scala3: String = "3.3.1" // Ready for cross build, currently not yet supported by play. -lazy val supportedScalaVersions: Seq[String] = Seq(scala213 /*, scala3*/) +lazy val scala3: String = "3.3.1" +lazy val supportedScalaVersions: Seq[String] = Seq(scala213, scala3) + +Global / evictionErrorLevel := Level.Info ThisBuild / description := "Authentication library for Play Framework applications that supports several authentication methods, including OAuth1, OAuth2, OpenID, CAS, Credentials, Basic Authentication, Two Factor Authentication or custom authentication schemes" ThisBuild / homepage := Some(url("https://silhouette.readme.io/")) @@ -17,17 +19,21 @@ ThisBuild / organizationName := "honeycomb-cheesecake" ThisBuild / scalaVersion := scala213 ThisBuild / versionScheme := Some("early-semver") ThisBuild / scalacOptions ++= Seq( - "-unchecked", - "-deprecation", "-feature", - "-encoding", "utf8", - "-Xfatal-warnings", - "-Xlint", - "-Xlint:adapted-args", - "-Xlint:inaccessible", - "-Xlint:infer-any", - "-Xlint:nullary-unit" -) + "-Xfatal-warnings" +) ++ + (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => Seq( + "-encoding", "utf8", + "-unchecked", + "-deprecation", + "-Xlint:adapted-args", + "-Xlint:inaccessible", + "-Xlint:infer-any", + "-Xlint:nullary-unit" + ) + case _ => Seq() + }) ThisBuild / Test / scalacOptions ~= { options: Seq[String] => // Allow dead code in tests (to support using mockito). options filterNot (_ == "-Ywarn-dead-code") @@ -98,8 +104,6 @@ lazy val root = (project in file(".")) lazy val silhouette = (project in file("silhouette")) .settings( name := "play-silhouette", - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-matcher-extra"), - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-mock"), libraryDependencies ++= Library.updates ++ Seq( Library.Play.cache, @@ -109,27 +113,26 @@ lazy val silhouette = (project in file("silhouette")) Library.apacheCommonLang, Library.Play.specs2 % Test, Library.Specs2.matcherExtra % Test, - Library.Specs2.mock % Test, + Library.mockito % Test, Library.scalaGuice % Test, Library.akkaTestkit % Test ), resolvers ++= Dependencies.resolvers ) .enablePlugins(PlayScala) + .disablePlugins(PlayAkkaHttpServer) lazy val silhouetteCas = (project in file("silhouette-cas")) .settings( name := "play-silhouette-cas", dependencyUpdatesFailBuild := false, - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-matcher-extra"), - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-mock"), libraryDependencies ++= Library.updates ++ Seq( Library.casClient, Library.casClientSupportSAML, Library.Play.specs2 % Test, Library.Specs2.matcherExtra % Test, - Library.Specs2.mock % Test, + Library.mockito % Test, Library.scalaGuice % Test ) ) @@ -139,7 +142,6 @@ lazy val silhouetteTotp = (project in file("silhouette-totp")) .settings( name := "play-silhouette-totp", dependencyUpdatesFailBuild := false, - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-core"), libraryDependencies ++= Library.updates ++ Seq( Library.googleAuth, @@ -152,8 +154,6 @@ lazy val silhouetteCryptoJca = (project in file("silhouette-crypto-jca")) .settings( name := "play-silhouette-crypto-jca", dependencyUpdatesFailBuild := false, - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-core"), - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-matcher-extra"), libraryDependencies ++= Library.updates ++ Seq( Library.commonsCodec, @@ -167,7 +167,6 @@ lazy val silhouetteArgon2 = (project in file("silhouette-password-argon2")) .settings( name := "play-silhouette-password-argon2", dependencyUpdatesFailBuild := false, - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-core"), libraryDependencies ++= Library.updates ++ Seq( Library.argon2, @@ -180,7 +179,6 @@ lazy val silhouetteBcrypt = (project in file("silhouette-password-bcrypt")) .settings( name := "play-silhouette-password-bcrypt", dependencyUpdatesFailBuild := false, - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-core"), libraryDependencies ++= Library.updates ++ Seq( Library.jbcrypt, @@ -193,14 +191,11 @@ lazy val silhouettePersistence = (project in file("silhouette-persistence")) .settings( name := "play-silhouette-persistence", dependencyUpdatesFailBuild := false, - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-core"), - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-matcher-extra"), - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-mock"), libraryDependencies ++= Library.updates ++ Seq( Library.Specs2.core % Test, Library.Specs2.matcherExtra % Test, - Library.Specs2.mock % Test, + Library.mockito % Test, Library.scalaGuice % Test ) ) @@ -210,17 +205,21 @@ lazy val silhouetteTestkit = (project in file("silhouette-testkit")) .settings( name := "play-silhouette-testkit", dependencyUpdatesFailBuild := false, - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-matcher-extra"), - dependencyUpdatesFilter -= moduleFilter(organization = "org.specs2", name = "specs2-mock"), libraryDependencies ++= Library.updates ++ Seq( Library.Play.test, Library.Play.specs2 % Test, Library.Specs2.matcherExtra % Test, - Library.Specs2.mock % Test, + Library.mockito % Test, Library.scalaGuice % Test, Library.akkaTestkit % Test ) + ++ { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => Seq(Library.izumiReflect) + case _ => Seq.empty + } + } ) .enablePlugins(PlayScala) .dependsOn(silhouette) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 4c8b4267..cb727e68 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -35,10 +35,9 @@ object Dependencies { } object Specs2 { - private val version = "4.20.2" // Versions later than this will fail due to removed dependencies. + private val version = "4.20.2" val core = "org.specs2" %% "specs2-core" % version val matcherExtra = "org.specs2" %% "specs2-matcher-extra" % version - val mock = "org.specs2" %% "specs2-mock" % version } val argon2 = "de.mkammerer" % "argon2-jvm" % "2.11" @@ -47,9 +46,11 @@ object Dependencies { val jwt = "com.auth0" % "java-jwt" % "3.18.2" val scalaGuice = "net.codingwell" %% "scala-guice" % "6.0.0" val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % play.core.PlayVersion.akkaVersion + val mockito = "org.mockito" % "mockito-core" % "5.3.0" val casClient = "org.jasig.cas.client" % "cas-client-core" % "3.6.4" val casClientSupportSAML = "org.jasig.cas.client" % "cas-client-support-saml" % "3.6.4" val apacheCommonLang = "org.apache.commons" % "commons-lang3" % "3.13.0" val googleAuth = "com.warrenstrange" % "googleauth" % "1.5.0" + val izumiReflect = "dev.zio" %% "izumi-reflect" % "2.3.8" // Scala 3 replacement for scala 2 reflect universe } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 28b04e59..f7e3e146 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin(dependency = "com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin(dependency = "com.jsuereth" % "sbt-pgp" % "2.1.1") addSbtPlugin(dependency = "com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin(dependency = "com.typesafe.play" % "sbt-plugin" % "2.9.0") -addSbtPlugin(dependency = "net.vonbuchholtz" % "sbt-dependency-check" % "4.0.0") +addSbtPlugin(dependency = "net.vonbuchholtz" % "sbt-dependency-check" % "5.1.0") addSbtPlugin(dependency = "org.scoverage" % "sbt-scoverage" % "2.0.9") addSbtPlugin(dependency = "org.scoverage" % "sbt-coveralls" % "1.3.11") addSbtPlugin(dependency = "org.xerial.sbt" % "sbt-sonatype" % "3.10.0") diff --git a/silhouette-cas/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/CasProviderSpec.scala b/silhouette-cas/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/CasProviderSpec.scala index 3d0a93b9..924ee00a 100644 --- a/silhouette-cas/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/CasProviderSpec.scala +++ b/silhouette-cas/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/CasProviderSpec.scala @@ -20,7 +20,7 @@ import io.github.honeycombcheesecake.play.silhouette.api.util.HTTPLayer import io.github.honeycombcheesecake.play.silhouette.api.{ Logger, LoginInfo } import org.jasig.cas.client.authentication.AttributePrincipal -import org.specs2.mock.Mockito +import org.mockito.Mockito._ import org.specs2.specification.Scope import play.api.mvc.AnyContentAsEmpty import play.api.test.FakeRequest @@ -33,7 +33,7 @@ import scala.concurrent.duration._ /** * Test case for the [[CasProvider]] class. */ -class CasProviderSpec extends SocialProviderSpec[CasInfo] with Mockito with Logger { +class CasProviderSpec extends SocialProviderSpec[CasInfo] with Logger { "The settings" should { "fail with a ConfigurationException if casURL is invalid" in new Context { @@ -95,9 +95,9 @@ class CasProviderSpec extends SocialProviderSpec[CasInfo] with Mockito with Logg "The `retrieveProfile` method" should { "return a valid profile if the CAS client validates the ticket" in new Context { - principal.getName returns userName - principal.getAttributes returns attr - client.validateServiceTicket(ticket) returns Future.successful(principal) + when(principal.getName).thenReturn(userName) + when(principal.getAttributes).thenReturn(attr) + when(client.validateServiceTicket(ticket)).thenReturn(Future.successful(principal)) implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/?ticket=%s".format(ticket)) @@ -129,8 +129,8 @@ class CasProviderSpec extends SocialProviderSpec[CasInfo] with Mockito with Logg redirectURL = "https://cas-redirect/") lazy val httpLayer = { - val m = mock[HTTPLayer] - m.executionContext returns global + val m = mock(classOf[HTTPLayer]) + when(m.executionContext).thenReturn(global) m } @@ -144,7 +144,7 @@ class CasProviderSpec extends SocialProviderSpec[CasInfo] with Mockito with Logg lazy val casAuthInfo = CasInfo(ticket) - lazy val principal = mock[AttributePrincipal].smart + lazy val principal = mock(classOf[AttributePrincipal], withSettings().defaultAnswer(RETURNS_SMART_NULLS)) lazy val name = "abc123" lazy val email = "email" diff --git a/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/daos/InMemoryAuthInfoDAOSpec.scala b/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/daos/InMemoryAuthInfoDAOSpec.scala index 9e2ed65c..a2a7025c 100644 --- a/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/daos/InMemoryAuthInfoDAOSpec.scala +++ b/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/daos/InMemoryAuthInfoDAOSpec.scala @@ -18,7 +18,6 @@ package io.github.honeycombcheesecake.play.silhouette.persistence.daos import io.github.honeycombcheesecake.play.silhouette.api.{ AuthInfo, LoginInfo } import io.github.honeycombcheesecake.play.silhouette.test.WaitPatience import org.specs2.concurrent.ExecutionEnv -import org.specs2.control.NoLanguageFeatures import org.specs2.mutable.Specification import org.specs2.specification.Scope @@ -29,7 +28,7 @@ import scala.language.postfixOps /** * Test case for the [[InMemoryAuthInfoDAO]] trait. */ -class InMemoryAuthInfoDAOSpec(implicit ev: ExecutionEnv) extends Specification with NoLanguageFeatures with WaitPatience { +class InMemoryAuthInfoDAOSpec(implicit ev: ExecutionEnv) extends Specification with WaitPatience { "The `find` method" should { "find an OAuth1 info for the given login info" in new Context { diff --git a/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/CacheAuthenticatorRepositorySpec.scala b/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/CacheAuthenticatorRepositorySpec.scala index df805c82..781e0f24 100644 --- a/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/CacheAuthenticatorRepositorySpec.scala +++ b/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/CacheAuthenticatorRepositorySpec.scala @@ -19,9 +19,9 @@ import io.github.honeycombcheesecake.play.silhouette.api.StorableAuthenticator import io.github.honeycombcheesecake.play.silhouette.api.util.CacheLayer import io.github.honeycombcheesecake.play.silhouette.test.WaitPatience import org.specs2.concurrent.ExecutionEnv -import org.specs2.mock.Mockito import org.specs2.mutable.Specification import org.specs2.specification.Scope +import org.mockito.Mockito._ import scala.concurrent.Future import scala.concurrent.duration.Duration @@ -29,50 +29,50 @@ import scala.concurrent.duration.Duration /** * Test case for the [[CacheAuthenticatorRepository]] class. */ -class CacheAuthenticatorRepositorySpec(implicit ev: ExecutionEnv) extends Specification with Mockito with WaitPatience { +class CacheAuthenticatorRepositorySpec(implicit ev: ExecutionEnv) extends Specification with WaitPatience { "The `find` method" should { "return value from cache" in new Context { - cacheLayer.find[StorableAuthenticator]("test-id") returns Future.successful(Some(authenticator)) + when(cacheLayer.find[StorableAuthenticator]("test-id")).thenReturn(Future.successful(Some(authenticator))) repository.find("test-id") must beSome(authenticator).awaitWithPatience - there was one(cacheLayer).find[StorableAuthenticator]("test-id") + verify(cacheLayer).find[StorableAuthenticator]("test-id") } "return None if value couldn't be found in cache" in new Context { - cacheLayer.find[StorableAuthenticator]("test-id") returns Future.successful(None) + when(cacheLayer.find[StorableAuthenticator]("test-id")).thenReturn(Future.successful(None)) repository.find("test-id") must beNone.awaitWithPatience - there was one(cacheLayer).find[StorableAuthenticator]("test-id") + verify(cacheLayer).find[StorableAuthenticator]("test-id") } } "The `add` method" should { "add value in cache" in new Context { - authenticator.id returns "test-id" - cacheLayer.save("test-id", authenticator, Duration.Inf) returns Future.successful(authenticator) + when(authenticator.id).thenReturn("test-id") + when(cacheLayer.save("test-id", authenticator, Duration.Inf)).thenReturn(Future.successful(authenticator)) repository.add(authenticator) must beEqualTo(authenticator).awaitWithPatience - there was one(cacheLayer).save("test-id", authenticator, Duration.Inf) + verify(cacheLayer).save("test-id", authenticator, Duration.Inf) } } "The `update` method" should { "update value in cache" in new Context { - authenticator.id returns "test-id" - cacheLayer.save("test-id", authenticator, Duration.Inf) returns Future.successful(authenticator) + when(authenticator.id).thenReturn("test-id") + when(cacheLayer.save("test-id", authenticator, Duration.Inf)).thenReturn(Future.successful(authenticator)) repository.update(authenticator) must beEqualTo(authenticator).awaitWithPatience - there was one(cacheLayer).save("test-id", authenticator, Duration.Inf) + verify(cacheLayer).save("test-id", authenticator, Duration.Inf) } } "The `remove` method" should { "remove value from cache" in new Context { - cacheLayer.remove("test-id") returns Future.successful(()) + when(cacheLayer.remove("test-id")).thenReturn(Future.successful(())) repository.remove("test-id") must beEqualTo(()).awaitWithPatience - there was one(cacheLayer).remove("test-id") + verify(cacheLayer).remove("test-id") } } @@ -84,12 +84,12 @@ class CacheAuthenticatorRepositorySpec(implicit ev: ExecutionEnv) extends Specif /** * A storable authenticator. */ - lazy val authenticator = mock[StorableAuthenticator] + lazy val authenticator = mock(classOf[StorableAuthenticator]) /** * The cache layer implementation. */ - lazy val cacheLayer = mock[CacheLayer] + lazy val cacheLayer = mock(classOf[CacheLayer]) /** * The repository to test. diff --git a/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/DelegableAuthInfoRepositorySpec.scala b/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/DelegableAuthInfoRepositorySpec.scala index eeefc3df..83fe6f96 100644 --- a/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/DelegableAuthInfoRepositorySpec.scala +++ b/silhouette-persistence/src/test/scala/io/github/honeycombcheesecake/play/silhouette/persistence/repositories/DelegableAuthInfoRepositorySpec.scala @@ -26,10 +26,9 @@ import io.github.honeycombcheesecake.play.silhouette.persistence.repositories.De import io.github.honeycombcheesecake.play.silhouette.test.WaitPatience import net.codingwell.scalaguice.ScalaModule import org.specs2.concurrent.ExecutionEnv -import org.specs2.control.NoLanguageFeatures -import org.specs2.mock.Mockito import org.specs2.mutable.Specification import org.specs2.specification.Scope +import org.mockito.Mockito._ import scala.concurrent.Await import scala.concurrent.duration._ @@ -39,7 +38,7 @@ import scala.language.postfixOps * Test case for the [[DelegableAuthInfoRepository]] trait. */ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) - extends Specification with Mockito with NoLanguageFeatures with WaitPatience { + extends Specification with WaitPatience { "The `find` method" should { "delegate the PasswordInfo to the correct DAO" in new Context { @@ -48,7 +47,7 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) Await.result(passwordInfoDAO.add(loginInfo, passwordInfo), 10 seconds) service.find[PasswordInfo](loginInfo) must beSome(passwordInfo).awaitWithPatience - there was one(passwordInfoDAO).find(loginInfo) + verify(passwordInfoDAO).find(loginInfo) } "delegate the OAuth1Info to the correct DAO" in new Context { @@ -57,7 +56,7 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) Await.result(oauth1InfoDAO.add(loginInfo, oauth1Info), 10 seconds) service.find[OAuth1Info](loginInfo) must beSome(oauth1Info).awaitWithPatience - there was one(oauth1InfoDAO).find(loginInfo) + verify(oauth1InfoDAO).find(loginInfo) } "delegate the OAuth2Info to the correct DAO" in new Context { @@ -66,7 +65,7 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) Await.result(oauth2InfoDAO.add(loginInfo, oauth2Info), 10 seconds) service.find[OAuth2Info](loginInfo) must beSome(oauth2Info).awaitWithPatience - there was one(oauth2InfoDAO).find(loginInfo) + verify(oauth2InfoDAO).find(loginInfo) } "throw a ConfigurationException if an unsupported type was given" in new Context { @@ -83,21 +82,21 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) val loginInfo = LoginInfo("credentials", "1") service.add(loginInfo, passwordInfo) must beEqualTo(passwordInfo).awaitWithPatience - there was one(passwordInfoDAO).add(loginInfo, passwordInfo) + verify(passwordInfoDAO).add(loginInfo, passwordInfo) } "delegate the OAuth1Info to the correct DAO" in new Context { val loginInfo = LoginInfo("credentials", "1") service.add(loginInfo, oauth1Info) must beEqualTo(oauth1Info).awaitWithPatience - there was one(oauth1InfoDAO).add(loginInfo, oauth1Info) + verify(oauth1InfoDAO).add(loginInfo, oauth1Info) } "delegate the OAuth2Info to the correct DAO" in new Context { val loginInfo = LoginInfo("credentials", "1") service.add(loginInfo, oauth2Info) must beEqualTo(oauth2Info).awaitWithPatience - there was one(oauth2InfoDAO).add(loginInfo, oauth2Info) + verify(oauth2InfoDAO).add(loginInfo, oauth2Info) } "throw a ConfigurationException if an unsupported type was given" in new Context { @@ -114,21 +113,21 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) val loginInfo = LoginInfo("credentials", "1") service.update(loginInfo, passwordInfo) must beEqualTo(passwordInfo).awaitWithPatience - there was one(passwordInfoDAO).update(loginInfo, passwordInfo) + verify(passwordInfoDAO).update(loginInfo, passwordInfo) } "delegate the OAuth1Info to the correct DAO" in new Context { val loginInfo = LoginInfo("credentials", "1") service.update(loginInfo, oauth1Info) must beEqualTo(oauth1Info).awaitWithPatience - there was one(oauth1InfoDAO).update(loginInfo, oauth1Info) + verify(oauth1InfoDAO).update(loginInfo, oauth1Info) } "delegate the OAuth2Info to the correct DAO" in new Context { val loginInfo = LoginInfo("credentials", "1") service.update(loginInfo, oauth2Info) must beEqualTo(oauth2Info).awaitWithPatience - there was one(oauth2InfoDAO).update(loginInfo, oauth2Info) + verify(oauth2InfoDAO).update(loginInfo, oauth2Info) } "throw a ConfigurationException if an unsupported type was given" in new Context { @@ -145,21 +144,21 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) val loginInfo = LoginInfo("credentials", "1") service.save(loginInfo, passwordInfo) must beEqualTo(passwordInfo).awaitWithPatience - there was one(passwordInfoDAO).save(loginInfo, passwordInfo) + verify(passwordInfoDAO).save(loginInfo, passwordInfo) } "delegate the OAuth1Info to the correct DAO" in new Context { val loginInfo = LoginInfo("credentials", "1") service.save(loginInfo, oauth1Info) must beEqualTo(oauth1Info).awaitWithPatience - there was one(oauth1InfoDAO).save(loginInfo, oauth1Info) + verify(oauth1InfoDAO).save(loginInfo, oauth1Info) } "delegate the OAuth2Info to the correct DAO" in new Context { val loginInfo = LoginInfo("credentials", "1") service.save(loginInfo, oauth2Info) must beEqualTo(oauth2Info).awaitWithPatience - there was one(oauth2InfoDAO).save(loginInfo, oauth2Info) + verify(oauth2InfoDAO).save(loginInfo, oauth2Info) } "throw a ConfigurationException if an unsupported type was given" in new Context { @@ -178,7 +177,7 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) Await.result(passwordInfoDAO.add(loginInfo, passwordInfo), 10 seconds) service.remove[PasswordInfo](loginInfo) must beEqualTo(()).awaitWithPatience - there was one(passwordInfoDAO).remove(loginInfo) + verify(passwordInfoDAO).remove(loginInfo) } "delegate the OAuth1Info to the correct DAO" in new Context { @@ -187,7 +186,7 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) Await.result(oauth1InfoDAO.add(loginInfo, oauth1Info), 10 seconds) service.remove[OAuth1Info](loginInfo) must beEqualTo(()).awaitWithPatience - there was one(oauth1InfoDAO).remove(loginInfo) + verify(oauth1InfoDAO).remove(loginInfo) } "delegate the OAuth2Info to the correct DAO" in new Context { @@ -196,7 +195,7 @@ class DelegableAuthInfoRepositorySpec(implicit ev: ExecutionEnv) Await.result(oauth2InfoDAO.add(loginInfo, oauth2Info), 10 seconds) service.remove[OAuth2Info](loginInfo) must beEqualTo(()).awaitWithPatience - there was one(oauth2InfoDAO).remove(loginInfo) + verify(oauth2InfoDAO).remove(loginInfo) } "throw a ConfigurationException if an unsupported type was given" in new Context { diff --git a/silhouette-testkit/app/io/github/honeycombcheesecake/play/silhouette/test/Fakes.scala b/silhouette-testkit/app-2/io/github/honeycombcheesecake/play/silhouette/test/Fakes.scala similarity index 100% rename from silhouette-testkit/app/io/github/honeycombcheesecake/play/silhouette/test/Fakes.scala rename to silhouette-testkit/app-2/io/github/honeycombcheesecake/play/silhouette/test/Fakes.scala diff --git a/silhouette-testkit/app/io/github/honeycombcheesecake/play/silhouette/test/package.scala b/silhouette-testkit/app-2/io/github/honeycombcheesecake/play/silhouette/test/package.scala similarity index 100% rename from silhouette-testkit/app/io/github/honeycombcheesecake/play/silhouette/test/package.scala rename to silhouette-testkit/app-2/io/github/honeycombcheesecake/play/silhouette/test/package.scala diff --git a/silhouette-testkit/app-3/io/github/honeycombcheesecake/play/silhouette/test/Fakes.scala b/silhouette-testkit/app-3/io/github/honeycombcheesecake/play/silhouette/test/Fakes.scala new file mode 100644 index 00000000..09000612 --- /dev/null +++ b/silhouette-testkit/app-3/io/github/honeycombcheesecake/play/silhouette/test/Fakes.scala @@ -0,0 +1,245 @@ +/** + * Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette.test + +import java.util.UUID +import io.github.honeycombcheesecake.play.silhouette.api._ +import io.github.honeycombcheesecake.play.silhouette.api.crypto.{ Base64AuthenticatorEncoder, Signer } +import io.github.honeycombcheesecake.play.silhouette.api.repositories.AuthenticatorRepository +import io.github.honeycombcheesecake.play.silhouette.api.services.{ AuthenticatorService, IdentityService } +import io.github.honeycombcheesecake.play.silhouette.api.util.Clock +import io.github.honeycombcheesecake.play.silhouette.impl.authenticators._ +import io.github.honeycombcheesecake.play.silhouette.impl.util.{ DefaultFingerprintGenerator, SecureRandomIDGenerator } +import izumi.reflect.Tag +import play.api.mvc.{ DefaultCookieHeaderEncoding, DefaultSessionCookieBaker, RequestHeader } + +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.{ ExecutionContext, Future } +import scala.util +import scala.util.Success + +/** + * A fake identity. + * + * @param loginInfo The linked login info for an identity. + */ +final case class FakeIdentity(loginInfo: LoginInfo) extends Identity + +/** + * A fake identity service implementation which can handle a predefined list of identities. + * + * @param identities A list of (login info -> identity) pairs this service is responsible for. + * @tparam I The type of the identity to handle. + */ +class FakeIdentityService[I <: Identity](identities: (LoginInfo, I)*) extends IdentityService[I] { + + /** + * Retrieves an identity that matches the specified login info. + * + * @param loginInfo The login info to retrieve an identity. + * @return The retrieved identity or None if no identity could be retrieved for the given login info. + */ + def retrieve(loginInfo: LoginInfo): Future[Option[I]] = { + Future.successful(identities.find(_._1 == loginInfo).map(_._2)) + } +} + +/** + * A fake authenticator repository which persists authenticators in memory. + * + * @tparam T The type of the authenticator to handle. + */ +class FakeAuthenticatorRepository[T <: StorableAuthenticator] extends AuthenticatorRepository[T] { + + /** + * The data store for the OAuth1 info. + */ + var data: mutable.HashMap[String, T] = mutable.HashMap() + + /** + * Finds the authenticator for the given ID. + * + * @param id The authenticator ID. + * @return The found authenticator or None if no authenticator could be found for the given ID. + */ + def find(id: String): Future[Option[T]] = { + Future.successful(data.get(id)) + } + + /** + * Adds a new authenticator. + * + * @param authenticator The authenticator to add. + * @return The added authenticator. + */ + def add(authenticator: T): Future[T] = { + data += (authenticator.id -> authenticator) + Future.successful(authenticator) + } + + /** + * Updates an already existing authenticator. + * + * @param authenticator The authenticator to update. + * @return The updated authenticator. + */ + def update(authenticator: T): Future[T] = { + data += (authenticator.id -> authenticator) + Future.successful(authenticator) + } + + /** + * Removes the authenticator for the given ID. + * + * @param id The authenticator ID. + * @return An empty future. + */ + def remove(id: String): Future[Unit] = { + data -= id + Future.successful(()) + } +} + +/** + * A fake session authenticator service. + */ +case object FakeSessionAuthenticatorService extends SessionAuthenticatorService( + SessionAuthenticatorSettings(), + new DefaultFingerprintGenerator(), + new Base64AuthenticatorEncoder, + new DefaultSessionCookieBaker(), + Clock()) + +/** + * A fake cookie authenticator service. + */ +case object FakeCookieAuthenticatorService extends CookieAuthenticatorService( + CookieAuthenticatorSettings(), + None, + new Signer { + def sign(data: String): String = data + def extract(message: String): util.Try[String] = Success(message) + }, + new DefaultCookieHeaderEncoding(), + new Base64AuthenticatorEncoder, + new DefaultFingerprintGenerator(), + new SecureRandomIDGenerator(), + Clock()) + +/** + * A fake bearer token authenticator service. + */ +case object FakeBearerTokenAuthenticatorService extends BearerTokenAuthenticatorService( + BearerTokenAuthenticatorSettings(), + new FakeAuthenticatorRepository[BearerTokenAuthenticator], + new SecureRandomIDGenerator(), + Clock()) + +/** + * A fake JWT authenticator service. + */ +case object FakeJWTAuthenticatorService extends JWTAuthenticatorService( + JWTAuthenticatorSettings(sharedSecret = UUID.randomUUID().toString), + None, + new Base64AuthenticatorEncoder, + new SecureRandomIDGenerator(), + Clock()) + +/** + * A fake Dummy authenticator service. + */ +case object FakeDummyAuthenticatorService extends DummyAuthenticatorService + +/** + * A fake authenticator service factory. + */ +object FakeAuthenticatorService { + + /** + * Creates a new fake authenticator for the given authenticator type. + * + * @tparam T The type of the authenticator. + * @return A fully configured authenticator instance. + */ + def apply[T <: Authenticator: Tag](): AuthenticatorService[T] = { + (Tag[T] match { + case t if t <:< Tag[SessionAuthenticator] => FakeSessionAuthenticatorService + case t if t <:< Tag[CookieAuthenticator] => FakeCookieAuthenticatorService + case t if t <:< Tag[BearerTokenAuthenticator] => FakeBearerTokenAuthenticatorService + case t if t <:< Tag[JWTAuthenticator] => FakeJWTAuthenticatorService + case t if t <:< Tag[DummyAuthenticator] => FakeDummyAuthenticatorService + case _ => throw new Exception("Invalid type specified.") + }).asInstanceOf[AuthenticatorService[T]] + } +} + +/** + * A fake authenticator. + * + * @param loginInfo The linked login info for an identity. + * @param id The ID of the authenticator. + * @param isValid True if the authenticator is valid, false otherwise. + */ +final case class FakeAuthenticator(loginInfo: LoginInfo, id: String = UUID.randomUUID().toString, isValid: Boolean = true) extends StorableAuthenticator + +/** + * A fake authenticator factory. + */ +object FakeAuthenticator { + + /** + * Creates a new fake authenticator for the given authenticator type. + * + * @param loginInfo The login info for which the authenticator should be created. + * @param env The Silhouette environment. + * @param requestHeader The request header. + * @tparam E The type of the environment, + * @return A authenticator instance. + */ + def apply[E <: Env](loginInfo: LoginInfo)(implicit env: Environment[E], requestHeader: RequestHeader): A[E] = { + env.authenticatorService.create(loginInfo) + } +} + +/** + * A fake environment implementation. + * + * @param identities A list of (login info -> identity) pairs to return inside a Silhouette action. + * @param requestProviders The list of request providers. + * @param eventBus The event bus implementation. + * @param executionContext The execution context to handle the asynchronous operations. + * @param tt The type tag of the authenticator type. + * @tparam E The type of the environment. + */ +final case class FakeEnvironment[E <: Env]( + identities: Seq[(LoginInfo, I[E])], + requestProviders: Seq[RequestProvider] = Seq.empty, + eventBus: EventBus = EventBus())( + implicit + val executionContext: ExecutionContext, + tt: Tag[A[E]]) extends Environment[E] { + + /** + * The identity service implementation. + */ + val identityService: IdentityService[I[E]] = new FakeIdentityService[I[E]](identities: _*) + + /** + * The authenticator service implementation. + */ + val authenticatorService: AuthenticatorService[A[E]] = FakeAuthenticatorService[A[E]]() +} diff --git a/silhouette-testkit/app-3/io/github/honeycombcheesecake/play/silhouette/test/package.scala b/silhouette-testkit/app-3/io/github/honeycombcheesecake/play/silhouette/test/package.scala new file mode 100644 index 00000000..0c51c197 --- /dev/null +++ b/silhouette-testkit/app-3/io/github/honeycombcheesecake/play/silhouette/test/package.scala @@ -0,0 +1,76 @@ +/** + * Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette + +import io.github.honeycombcheesecake.play.silhouette.api._ +import play.api.mvc.Request +import play.api.test.FakeRequest + +import scala.concurrent.duration._ +import scala.concurrent.{ Await, Future } +import scala.language.{ implicitConversions, postfixOps } + +/** + * Test helpers to test a Silhouette application. + */ +package object test { + + /** + * Resolves a future by waiting of the result. + * + * @param f The future to block. + * @tparam T The type contained in the future. + * @return The value contained in the future. + */ + implicit protected[test] def await[T](f: Future[T]): T = Await.result(f, 60 seconds) + + /** + * Provides a method which add an authenticator to a fake request. + * + * @param f A fake request instance. + * @tparam A The type of the body. + */ + implicit class FakeRequestWithAuthenticator[F](f: FakeRequest[F]) { + implicit val request: FakeRequest[F] = f + + /** + * Creates a fake request with an embedded authenticator. + * + * @param authenticator The authenticator to embed into the request. + * @param env The Silhouette environment. + * @tparam E The type of the environment. + * @return A fake request. + */ + def withAuthenticator[E <: Env](authenticator: A[E])(implicit env: Environment[E]): FakeRequest[F] = { + implicit val ec = env.executionContext + val rh = env.authenticatorService.init(authenticator).map(v => env.authenticatorService.embed(v, f)) + + new FakeRequest(Request.apply(rh, f.body)) + } + + /** + * Creates a fake request with an embedded authenticator. + * + * @param loginInfo The login info for which the new authenticator should be created. + * @param env The Silhouette environment. + * @tparam E The type of the environment. + * @return A fake request. + */ + def withAuthenticator[E <: Env](loginInfo: LoginInfo)(implicit env: Environment[E]): FakeRequest[F] = { + withAuthenticator(FakeAuthenticator[E](loginInfo)) + } + } +} diff --git a/silhouette-testkit/test/io/github/honeycombcheesecake/play/silhouette/test/FakesSpec.scala b/silhouette-testkit/test/io/github/honeycombcheesecake/play/silhouette/test/FakesSpec.scala index 00461e08..1555897c 100644 --- a/silhouette-testkit/test/io/github/honeycombcheesecake/play/silhouette/test/FakesSpec.scala +++ b/silhouette-testkit/test/io/github/honeycombcheesecake/play/silhouette/test/FakesSpec.scala @@ -15,17 +15,21 @@ */ package io.github.honeycombcheesecake.play.silhouette.test +import com.google.inject.AbstractModule + import javax.inject.Inject import io.github.honeycombcheesecake.play.silhouette.api._ +import io.github.honeycombcheesecake.play.silhouette.api.actions.{ SecuredRequest, UserAwareRequest } import io.github.honeycombcheesecake.play.silhouette.impl.authenticators._ import io.github.honeycombcheesecake.play.silhouette.test.FakesSpec._ import net.codingwell.scalaguice.ScalaModule import org.specs2.matcher.JsonMatchers import org.specs2.specification.Scope import play.api.inject.guice.GuiceApplicationBuilder +import play.api.inject.guice.GuiceableModule import play.api.libs.json.Json -import play.api.mvc.{ AbstractController, AnyContentAsEmpty, ControllerComponents } +import play.api.mvc.{ AbstractController, AnyContent, AnyContentAsEmpty, ControllerComponents } import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } import scala.concurrent.ExecutionContext.Implicits.global @@ -109,7 +113,9 @@ class FakesSpec extends PlaySpecification with JsonMatchers { } "return a `CookieAuthenticatorService`" in new WithApplication { - FakeAuthenticatorService[CookieAuthenticator]() must beAnInstanceOf[CookieAuthenticatorService] + override def running() = { + FakeAuthenticatorService[CookieAuthenticator]() must beAnInstanceOf[CookieAuthenticatorService] + } } "return a `BearerTokenAuthenticatorService`" in { @@ -127,92 +133,108 @@ class FakesSpec extends PlaySpecification with JsonMatchers { "The `FakeAuthenticator` factory" should { "return a `SessionAuthenticator`" in new WithApplication { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[SessionEnv] = FakeEnvironment[SessionEnv](Seq(loginInfo -> identity)) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[SessionEnv] = FakeEnvironment[SessionEnv](Seq(loginInfo -> identity)) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - FakeAuthenticator(loginInfo) must beAnInstanceOf[SessionAuthenticator] + FakeAuthenticator(loginInfo) must beAnInstanceOf[SessionAuthenticator] + } } "return a `CookieAuthenticator`" in new WithApplication { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - FakeAuthenticator(loginInfo) must beAnInstanceOf[CookieAuthenticator] + FakeAuthenticator(loginInfo) must beAnInstanceOf[CookieAuthenticator] + } } "return a `BearerTokenAuthenticator`" in new WithApplication { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[BearerTokenEnv] = FakeEnvironment[BearerTokenEnv](Seq(loginInfo -> identity)) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[BearerTokenEnv] = FakeEnvironment[BearerTokenEnv](Seq(loginInfo -> identity)) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - FakeAuthenticator(loginInfo) must beAnInstanceOf[BearerTokenAuthenticator] + FakeAuthenticator(loginInfo) must beAnInstanceOf[BearerTokenAuthenticator] + } } "return a `JWTAuthenticator`" in new WithApplication { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[JWTEnv] = FakeEnvironment[JWTEnv](Seq(loginInfo -> identity)) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[JWTEnv] = FakeEnvironment[JWTEnv](Seq(loginInfo -> identity)) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - FakeAuthenticator(loginInfo) must beAnInstanceOf[JWTAuthenticator] + FakeAuthenticator(loginInfo) must beAnInstanceOf[JWTAuthenticator] + } } "return a `DummyAuthenticator`" in new WithApplication { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[DummyEnv] = FakeEnvironment[DummyEnv](Seq(loginInfo -> identity)) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[DummyEnv] = FakeEnvironment[DummyEnv](Seq(loginInfo -> identity)) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - FakeAuthenticator(loginInfo) must beAnInstanceOf[DummyAuthenticator] + FakeAuthenticator(loginInfo) must beAnInstanceOf[DummyAuthenticator] + } } } "The `securedAction` method of the `SecuredController`" should { "return a 401 status code if no authenticator was found" in new InjectorContext { new WithApplication(app) { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - val env = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) - val request = FakeRequest() + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + val env = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) + val request = FakeRequest() - val controller = app.injector.instanceOf[SecuredController] - val result = controller.defaultSecuredAction(request) + val controller = app.injector.instanceOf[SecuredController] + val result = controller.defaultSecuredAction(request) - status(result) must equalTo(UNAUTHORIZED) + status(result) must equalTo(UNAUTHORIZED) + } } } "return a 401 status code if authenticator but no identity was found" in new InjectorContext { new WithApplication(app) { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) - val request = FakeRequest().withAuthenticator(LoginInfo("invalid", "invalid")) + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) + val request = FakeRequest().withAuthenticator(LoginInfo("invalid", "invalid")) - val controller = app.injector.instanceOf[SecuredController] - val result = controller.defaultSecuredAction(request) + val controller = app.injector.instanceOf[SecuredController] + val result = controller.defaultSecuredAction(request) - status(result) must equalTo(UNAUTHORIZED) + status(result) must equalTo(UNAUTHORIZED) + } } } "return a 200 status code if authenticator and identity was found" in new InjectorContext { new WithApplication(app) { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) - val request = FakeRequest().withAuthenticator(loginInfo) - - val controller = app.injector.instanceOf[SecuredController] - val result = controller.defaultSecuredAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "test") + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) + val request = FakeRequest().withAuthenticator(loginInfo) + + val controller = app.injector.instanceOf[SecuredController] + val result = controller.defaultSecuredAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "test") + } } } } @@ -220,45 +242,51 @@ class FakesSpec extends PlaySpecification with JsonMatchers { "The `userAwareAction` method of the `SecuredController`" should { "return a 401 status code if no authenticator was found" in new InjectorContext { new WithApplication(app) { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - val env = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) - val request = FakeRequest() + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + val env = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) + val request = FakeRequest() - val controller = app.injector.instanceOf[SecuredController] - val result = controller.defaultUserAwareAction(request) + val controller = app.injector.instanceOf[SecuredController] + val result = controller.defaultUserAwareAction(request) - status(result) must equalTo(UNAUTHORIZED) + status(result) must equalTo(UNAUTHORIZED) + } } } "return a 401 status code if authenticator but no identity was found" in new InjectorContext { new WithApplication(app) { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) - val request = FakeRequest().withAuthenticator(LoginInfo("invalid", "invalid")) + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) + implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) + val request = FakeRequest().withAuthenticator(LoginInfo("invalid", "invalid")) - val controller = app.injector.instanceOf[SecuredController] - val result = controller.defaultUserAwareAction(request) + val controller = app.injector.instanceOf[SecuredController] + val result = controller.defaultUserAwareAction(request) - status(result) must equalTo(UNAUTHORIZED) + status(result) must equalTo(UNAUTHORIZED) + } } } "return a 200 status code if authenticator and identity was found" in new InjectorContext { new WithApplication(app) { - val loginInfo = LoginInfo("test", "test") - val identity = FakeIdentity(loginInfo) + override def running() = { + val loginInfo = LoginInfo("test", "test") + val identity = FakeIdentity(loginInfo) - implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) - val request = FakeRequest().withAuthenticator(loginInfo) + implicit val env: FakeEnvironment[CookieEnv] = FakeEnvironment[CookieEnv](Seq(loginInfo -> identity)) + val request = FakeRequest().withAuthenticator(loginInfo) - val controller = app.injector.instanceOf[SecuredController] - val result = controller.defaultUserAwareAction(request) + val controller = app.injector.instanceOf[SecuredController] + val result = controller.defaultUserAwareAction(request) - status(result) must equalTo(OK) - contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "test") + status(result) must equalTo(OK) + contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "test") + } } } } @@ -287,13 +315,13 @@ class FakesSpec extends PlaySpecification with JsonMatchers { * The guice application builder. */ lazy val app = new GuiceApplicationBuilder() - .bindings(new GuiceModule) + .bindings(GuiceableModule.guiceable(new GuiceModule)) .build() /** * The guice module. */ - class GuiceModule extends ScalaModule { + class GuiceModule extends AbstractModule with ScalaModule { override def configure(): Unit = { bind[Silhouette[CookieEnv]].to[SilhouetteProvider[CookieEnv]] bind[Environment[CookieEnv]].toInstance(env) @@ -363,7 +391,7 @@ object FakesSpec { * * @return The result to send to the client. */ - def defaultSecuredAction = silhouette.SecuredAction { implicit request => + def defaultSecuredAction = silhouette.SecuredAction { implicit request: SecuredRequest[CookieEnv, AnyContent] => Ok(Json.toJson(request.identity.loginInfo)) } @@ -372,7 +400,7 @@ object FakesSpec { * * @return The result to send to the client. */ - def defaultUserAwareAction = silhouette.UserAwareAction { implicit request => + def defaultUserAwareAction = silhouette.UserAwareAction { implicit request: UserAwareRequest[CookieEnv, AnyContent] => request.identity match { case Some(identity) => Ok(Json.toJson(identity.loginInfo)) case None => Unauthorized diff --git a/silhouette-totp/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/GoogleTotpProviderSpec.scala b/silhouette-totp/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/GoogleTotpProviderSpec.scala index 0812cf18..fc4ca1e8 100644 --- a/silhouette-totp/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/GoogleTotpProviderSpec.scala +++ b/silhouette-totp/src/test/scala/io/github/honeycombcheesecake/play/silhouette/impl/providers/GoogleTotpProviderSpec.scala @@ -17,7 +17,8 @@ package io.github.honeycombcheesecake.play.silhouette.impl.providers import io.github.honeycombcheesecake.play.silhouette.api.util.{ Credentials, PasswordInfo } import com.warrenstrange.googleauth.GoogleAuthenticator -import org.specs2.mock.Mockito +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any import play.api.test.WithApplication import scala.concurrent.ExecutionContext.Implicits.global @@ -25,58 +26,76 @@ import scala.concurrent.ExecutionContext.Implicits.global /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.providers.GoogleTotpProvider#GoogleTOTPProvider]] class. */ -class GoogleTotpProviderSpec extends PasswordProviderSpec with Mockito { +class GoogleTotpProviderSpec extends PasswordProviderSpec { "The `authenticate` with verification code method" should { "return None when the sharedKey is null or empty" in new WithApplication with Context { - await(provider.authenticate(null.asInstanceOf[String], testVerificationCode)) should be(None) - await(provider.authenticate("", testVerificationCode)) should be(None) + override def running() = { + await(provider.authenticate(null.asInstanceOf[String], testVerificationCode)) should be(None) + await(provider.authenticate("", testVerificationCode)) should be(None) + } } "return None when the verification code is null or empty" in new WithApplication with Context { - await(provider.authenticate(testSharedKey, null)) should be(None) - await(provider.authenticate(testSharedKey, "")) should be(None) + override def running() = { + await(provider.authenticate(testSharedKey, null)) should be(None) + await(provider.authenticate(testSharedKey, "")) should be(None) + } } "return None when the verification code isn't a number" in new WithApplication with Context { - await(provider.authenticate(testSharedKey, testWrongVerificationCode)) should be(None) + override def running() = { + await(provider.authenticate(testSharedKey, testWrongVerificationCode)) should be(None) + } } "return valid `Some(TotpInfo)` when the verification code is correct" in new WithApplication with Context { - val googleAuthenticator = new GoogleAuthenticator() - val validVerificationCode = googleAuthenticator.getTotpPassword(testSharedKey) - await(provider.authenticate(testSharedKey, validVerificationCode.toString)) should not be empty + override def running() = { + val googleAuthenticator = new GoogleAuthenticator() + val validVerificationCode = googleAuthenticator.getTotpPassword(testSharedKey) + await(provider.authenticate(testSharedKey, validVerificationCode.toString)) should not be None + } } } "The `createCredentials` method" should { "return the correct TotpCredentials shared key" in new WithApplication with Context { - val result = provider.createCredentials(credentials.identifier) - result.totpInfo.sharedKey must not be "" - result.totpInfo.scratchCodes must not be empty - result.qrUrl must not be "" + override def running() = { + val result = provider.createCredentials(credentials.identifier) + result.totpInfo.sharedKey.nonEmpty must beTrue + result.totpInfo.scratchCodes.nonEmpty must beTrue + result.qrUrl.nonEmpty must beTrue + } } } "The `authenticate` with verification code method" should { "throw NullPointerException when the input totpInfo is null" in new WithApplication with Context { - await(provider.authenticate(null.asInstanceOf[GoogleTotpInfo], testWrongVerificationCode)) must throwA[NullPointerException] + override def running() = { + await(provider.authenticate(null.asInstanceOf[GoogleTotpInfo], testWrongVerificationCode)) must throwA[NullPointerException] + } } "return throw NullPointerException when the plain scratch code is null" in new WithApplication with Context { - val result = provider.createCredentials(credentials.identifier) - await(provider.authenticate(result.totpInfo, null.asInstanceOf[String])) must throwA[NullPointerException] + override def running() = { + val result = provider.createCredentials(credentials.identifier) + await(provider.authenticate(result.totpInfo, null.asInstanceOf[String])) must throwA[NullPointerException] + } } "return None when the plain scratch code is empty" in new WithApplication with Context { - val result = provider.createCredentials(credentials.identifier) - await(provider.authenticate(result.totpInfo, "")) should be(None) + override def running() = { + val result = provider.createCredentials(credentials.identifier) + await(provider.authenticate(result.totpInfo, "")) should be(None) + } } "return Some(PasswordInfo,TotpInfo) when the plain scratch code is valid" in new WithApplication with Context { - fooHasher.hash(any()) returns testPasswordInfo - barHasher.matches(testPasswordInfo, testScratchCode) returns true - val result = provider.createCredentials(credentials.identifier) - await(provider.authenticate(result.totpInfo, testScratchCode)) should not be empty + override def running() = { + when(fooHasher.hash(any())).thenReturn(testPasswordInfo) + when(barHasher.matches(testPasswordInfo, testScratchCode)).thenReturn(true) + val result = provider.createCredentials(credentials.identifier) + await(provider.authenticate(result.totpInfo, testScratchCode)) should not be None + } } } diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/Authenticator.scala b/silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/Authenticator.scala similarity index 100% rename from silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/Authenticator.scala rename to silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/Authenticator.scala diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/Environment.scala b/silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/Environment.scala similarity index 100% rename from silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/Environment.scala rename to silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/Environment.scala diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/RequestHandler.scala b/silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/RequestHandler.scala similarity index 100% rename from silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/RequestHandler.scala rename to silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/RequestHandler.scala diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredAction.scala b/silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredAction.scala similarity index 100% rename from silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredAction.scala rename to silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredAction.scala diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareAction.scala b/silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareAction.scala similarity index 100% rename from silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareAction.scala rename to silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareAction.scala diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorService.scala b/silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorService.scala similarity index 100% rename from silhouette/app/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorService.scala rename to silhouette/app-2/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorService.scala diff --git a/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/Authenticator.scala b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/Authenticator.scala new file mode 100644 index 00000000..5ab693ae --- /dev/null +++ b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/Authenticator.scala @@ -0,0 +1,175 @@ +/** + * Original work: SecureSocial (https://github.com/jaliss/securesocial) + * Copyright 2013 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss + * + * Derivative work: Silhouette (https://github.com/mohiva/play-silhouette) + * Modifications Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette.api + +import io.github.honeycombcheesecake.play.silhouette.api.Authenticator.Implicits._ + +import java.time.ZonedDateTime +import scala.concurrent.duration.FiniteDuration + +/** + * An authenticator recognizes an authenticated user. + */ +trait Authenticator { + + /** + * The Type of the generated value an authenticator will be serialized to. + */ + type Value + + /** + * The type of the settings an authenticator can handle. + */ + type Settings + + /** + * Gets the linked login info for an identity. + * + * @return The linked login info for an identity. + */ + def loginInfo: LoginInfo + + /** + * Checks if the authenticator valid. + * + * @return True if the authenticator valid, false otherwise. + */ + def isValid: Boolean +} + + +// match types +type Value[A <: Authenticator] = A match + case Authenticator.AuxV[v] => v // lower case is significant + +type Settings[A <: Authenticator] = A match + case Authenticator.AuxS[s] => s // lower case is significant + +/** + * The `Authenticator` companion object. + */ +object Authenticator { + + type AuxV[_V] = Authenticator {type Value = _V} + type AuxS[_S] = Authenticator {type Settings = _S} + + /** + * Some implicits. + */ + object Implicits { + + /** + * Defines additional methods on a `ZonedDateTime` instance. + * + * @param dateTime The `ZonedDateTime` instance on which the additional methods should be defined. + */ + implicit class RichDateTime(dateTime: ZonedDateTime) { + + /** + * Adds a duration to a date/time. + * + * @param duration The duration to add. + * @return A date/time instance with the added duration. + */ + def +(duration: FiniteDuration): ZonedDateTime = { + dateTime.plusSeconds(duration.toSeconds.toInt) + } + + /** + * Subtracts a duration from a date/time. + * + * @param duration The duration to subtract. + * @return A date/time instance with the subtracted duration. + */ + def -(duration: FiniteDuration): ZonedDateTime = { + dateTime.minusSeconds(duration.toSeconds.toInt) + } + + /** + * Compares a date/time with the current time + * + * @return Is the current time before the time supplied by the Clock + */ + def isBeforeNow: Boolean = { + dateTime.isBefore(ZonedDateTime.now) + } + } + } +} + +/** + * An authenticator which can be stored in a backing store. + */ +trait StorableAuthenticator extends Authenticator { + + /** + * Gets the ID to reference the authenticator in the backing store. + * + * @return The ID to reference the authenticator in the backing store. + */ + def id: String +} + +/** + * An authenticator that may expire. + */ +trait ExpirableAuthenticator extends Authenticator { + + /** + * The last used date/time. + */ + val lastUsedDateTime: ZonedDateTime + + /** + * The expiration date/time. + */ + val expirationDateTime: ZonedDateTime + + /** + * The duration an authenticator can be idle before it timed out. + */ + val idleTimeout: Option[FiniteDuration] + + /** + * Checks if the authenticator isn't expired and isn't timed out. + * + * @return True if the authenticator isn't expired and isn't timed out. + */ + override def isValid: Boolean = !isExpired && !isTimedOut + + /** + * Checks if the authenticator is expired. This is an absolute timeout since the creation of + * the authenticator. + * + * @return True if the authenticator is expired, false otherwise. + */ + def isExpired: Boolean = expirationDateTime.isBeforeNow + + /** + * Checks if the time elapsed since the last time the authenticator was used, is longer than + * the maximum idle timeout specified in the properties. + * + * @return True if sliding window expiration is activated and the authenticator is timed out, false otherwise. + */ + def isTimedOut: Boolean = idleTimeout match { + case Some(idle) => (lastUsedDateTime + idle).isBeforeNow + case _ => false + } +} diff --git a/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/Environment.scala b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/Environment.scala new file mode 100644 index 00000000..9f776d8c --- /dev/null +++ b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/Environment.scala @@ -0,0 +1,121 @@ +/** + * Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette.api + +import io.github.honeycombcheesecake.play.silhouette.api.services.{ AuthenticatorService, IdentityService } +import io.github.honeycombcheesecake.play.silhouette.api.util.ExecutionContextProvider + +import scala.concurrent.ExecutionContext + +/** + * The environment type. + * + * Defines the [[Identity]] and [[Authenticator]] types for an environment. It is possible + * to implement as many types as needed. This has the advantage that an application isn't + * bound only to a single `Identity` -> `Authenticator` combination. + * + * To define a new environment type create a new trait with the appropriate [[Identity]] and + * [[Authenticator]] types: + * + * {{{ + * trait SessionEnv extends Env { + * type I = User + * type A = SessionAuthenticator + * } + * trait JWTEnv extends Env { + * type I = User + * type A = JWTAuthenticator + * } + * }}} + */ +trait Env { + type I <: Identity + type A <: Authenticator +} + +object Env { + type AuxI[_I <: Identity] = Env {type I = _I} + type AuxA[_A <: Authenticator] = Env {type A = _A} +} + + // match types +type I[E <: Env] <: Identity = E match + case Env.AuxI[i] => i // lower case is significant + +type A[E <: Env] <: Authenticator = E match + case Env.AuxA[a] => a + +/** + * Provides the components needed to handle a secured request. + * + * It's possible to declare different environments for different environment types. The + * [[io.github.honeycombcheesecake.play.silhouette.api.services.IdentityService]] and the + * [[io.github.honeycombcheesecake.play.silhouette.api.services.AuthenticatorService]] are bound to the appropriate types + * defined in the environment type. But the [[EventBus]] and the list of [[RequestProvider]] + * instances can be defined as needed for every environment type. + */ +trait Environment[E <: Env] extends ExecutionContextProvider { + + /** + * Gets the identity service implementation. + * + * @return The identity service implementation. + */ + def identityService: IdentityService[I[E]] + + /** + * Gets the authenticator service implementation. + * + * @return The authenticator service implementation. + */ + def authenticatorService: AuthenticatorService[A[E]] + + /** + * Gets the list of request providers. + * + * @return The list of request providers. + */ + def requestProviders: Seq[RequestProvider] + + /** + * The event bus implementation. + * + * @return The event bus implementation. + */ + def eventBus: EventBus +} + +/** + * Companion object to easily create environment instances. + * + * {{{ + * Environment[SessionEnv](...) + * Environment[JWTEnv](...) + * }}} + */ +object Environment { + def apply[E <: Env]( + identityServiceImpl: IdentityService[I[E]], + authenticatorServiceImpl: AuthenticatorService[A[E]], + requestProvidersImpl: Seq[RequestProvider], + eventBusImpl: EventBus)(implicit ec: ExecutionContext) = new Environment[E] { + val identityService = identityServiceImpl + val authenticatorService = authenticatorServiceImpl + val requestProviders = requestProvidersImpl + val eventBus = eventBusImpl + val executionContext = ec + } +} diff --git a/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/RequestHandler.scala b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/RequestHandler.scala new file mode 100644 index 00000000..dd8872ff --- /dev/null +++ b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/RequestHandler.scala @@ -0,0 +1,227 @@ +/** + * Original work: SecureSocial (https://github.com/jaliss/securesocial) + * Copyright 2013 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss + * + * Derivative work: Silhouette (https://github.com/mohiva/play-silhouette) + * Modifications Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette.api + +import io.github.honeycombcheesecake.play.silhouette.api.services.AuthenticatorResult +import io.github.honeycombcheesecake.play.silhouette.api.util.ExecutionContextProvider +import play.api.mvc.{ Result, RequestHeader, Request, AnyContent } + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +/** + * A result which can transport a result as also additional data through the request handler process. + * + * @param result A Play Framework result. + * @param data Additional data to transport in the result. + * @tparam T The type of the data. + */ +final case class HandlerResult[+T](result: Result, data: Option[T] = None) + +/** + * Base implementation for building request handlers. + * + * The base implementations to handle secured endpoints are encapsulated into request handlers which + * can execute an arbitrary block of code and must return a HandlerResult. This HandlerResult consists + * of a normal Play result and arbitrary additional data which can be transported out of these handlers. + * + * @tparam E The type of the environment. + * @tparam R The type of the request. + */ +trait RequestHandlerBuilder[E <: Env, +R[_]] extends ExecutionContextProvider { + + /** + * Provides an `extract` method on an `Either` which contains the same types. + */ + protected implicit class ExtractEither[T](r: Either[T, T]) { + def extract: T = r.fold(identity, identity) + } + + /** + * The execution context to handle the asynchronous operations. + */ + implicit /*lazy*/ val executionContext: ExecutionContext = environment.executionContext + + /** + * The environment instance to handle the request. + */ + val environment: Environment[E] + + /** + * Constructs a request handler with default content. + * + * @param block The block of code to invoke. + * @param request The current request. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + final def apply[T](block: R[AnyContent] => Future[HandlerResult[T]])(implicit request: Request[AnyContent]): Future[HandlerResult[T]] = { + invokeBlock(block) + } + + /** + * Constructs a request handler with the content of the given request. + * + * @param request The current request. + * @param block The block of code to invoke. + * @tparam B The type of the request body. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + final def apply[B, T](request: Request[B])(block: R[B] => Future[HandlerResult[T]]): Future[HandlerResult[T]] = { + invokeBlock(block)(request) + } + + /** + * Invoke the block. + * + * This is the main method that an request handler has to implement. + * + * @param block The block of code to invoke. + * @param request The current request. + * @tparam B The type of the request body. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + def invokeBlock[B, T](block: R[B] => Future[HandlerResult[T]])(implicit request: Request[B]): Future[HandlerResult[T]] + + /** + * Handles a block for an authenticator. + * + * Invokes the block with the authenticator and handles the result. See `handleInitializedAuthenticator` and + * `handleUninitializedAuthenticator` methods too see how the different authenticator types will be handled. + * + * @param authenticator An already initialized authenticator on the left and a new authenticator on the right. + * @param block The block to handle with the authenticator. + * @param request The current request header. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + protected def handleBlock[T](authenticator: Either[A[E], A[E]], block: A[E] => Future[HandlerResult[T]])(implicit request: RequestHeader) = { + authenticator match { + case Left(a) => handleInitializedAuthenticator(a, block) + case Right(a) => handleUninitializedAuthenticator(a, block) + } + } + + /** + * Handles the authentication of an identity. + * + * As first it checks for authenticators in requests, then it tries to authenticate against a request provider. + * This method marks the returned authenticators by returning already initialized authenticators on the + * left and new authenticators on the right. All new authenticators must be initialized later in the flow, + * with the result returned from the invoked block. + * + * @param request The current request. + * @tparam B The type of the request body. + * @return A tuple which consists of (maybe the existing authenticator on the left or a + * new authenticator on the right -> maybe the identity). + */ + protected def handleAuthentication[B](implicit request: Request[B]): Future[(Option[Either[A[E], A[E]]], Option[I[E]])] = { + environment.authenticatorService.retrieve.flatMap { + // A valid authenticator was found so we retrieve also the identity + case Some(a) if a.isValid => environment.identityService.retrieve(a.loginInfo).map(i => Some(Left(a)) -> i) + // An invalid authenticator was found so we needn't retrieve the identity + case Some(a) => Future.successful(Some(Left(a)) -> None) + // No authenticator was found so we try to authenticate with a request provider + case None => handleRequestProviderAuthentication.flatMap { + // Authentication was successful, so we retrieve the identity and create a new authenticator for it + case Some(loginInfo) => environment.identityService.retrieve(loginInfo).flatMap { i => + environment.authenticatorService.create(loginInfo).map(a => Some(Right(a)) -> i) + } + // No identity and no authenticator was found + case None => Future.successful(None -> None) + } + } + } + + /** + * Handles already initialized authenticators. + * + * The authenticator handled by this method was found in the current request. So it was initialized on + * a previous request and must now be updated if it was touched and no authenticator result was found. + * + * @param authenticator The authenticator to handle. + * @param block The block to handle with the authenticator. + * @param request The current request header. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + private def handleInitializedAuthenticator[T](authenticator: A[E], block: A[E] => Future[HandlerResult[T]])(implicit request: RequestHeader) = { + val auth = environment.authenticatorService.touch(authenticator) + block(auth.fold(identity, identity)).flatMap { + case hr @ HandlerResult(pr: AuthenticatorResult, _) => Future.successful(hr) + case hr @ HandlerResult(pr, _) => auth match { + // Authenticator was touched so we update the authenticator and maybe the result + case Left(a) => environment.authenticatorService.update(a, pr).map(pr => hr.copy(pr)) + // Authenticator was not touched so we return the original result + case Right(a) => Future.successful(hr) + } + } + } + + /** + * Handles not initialized authenticators. + * + * The authenticator handled by this method was newly created after authentication with a request provider. + * So it must be initialized with the result of the invoked block if no authenticator result was found. + * + * @param authenticator The authenticator to handle. + * @param block The block to handle with the authenticator. + * @param request The current request header. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + private def handleUninitializedAuthenticator[T](authenticator: A[E], block: A[E] => Future[HandlerResult[T]])(implicit request: RequestHeader) = { + block(authenticator).flatMap { + case hr @ HandlerResult(pr: AuthenticatorResult, _) => Future.successful(hr) + case hr @ HandlerResult(pr, _) => + environment.authenticatorService.init(authenticator).flatMap { value => + environment.authenticatorService.embed(value, pr) + }.map(pr => hr.copy(pr)) + } + } + + /** + * Handles the authentication with the request providers. + * + * Silhouette supports chaining of request providers. So if more as one request provider is defined + * it tries to authenticate until one provider returns an identity. The order of the providers + * isn't guaranteed. + * + * @param request The current request. + * @tparam B The type of the request body. + * @return Some identity or None if authentication was not successful. + */ + private def handleRequestProviderAuthentication[B](implicit request: Request[B]): Future[Option[LoginInfo]] = { + def auth(providers: Seq[RequestProvider]): Future[Option[LoginInfo]] = { + providers match { + case Nil => Future.successful(None) + case h :: t => h.authenticate(request).flatMap { + case Some(i) => Future.successful(Some(i)) + case None => if (t.isEmpty) Future.successful(None) else auth(t) + } + case _ => Future.successful(None) + } + } + + auth(environment.requestProviders) + } +} diff --git a/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredAction.scala b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredAction.scala new file mode 100644 index 00000000..a8e3bbc7 --- /dev/null +++ b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredAction.scala @@ -0,0 +1,411 @@ +/** + * Original work: SecureSocial (https://github.com/jaliss/securesocial) + * Copyright 2013 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss + * + * Derivative work: Silhouette (https://github.com/mohiva/play-silhouette) + * Modifications Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette.api.actions + +import javax.inject.Inject + +import io.github.honeycombcheesecake.play.silhouette.api._ +import play.api.i18n.MessagesApi +import play.api.inject.Module +import play.api.mvc._ +import play.api.{ Configuration, Environment => PlayEnv } + +import scala.annotation.nowarn +import scala.concurrent.{ ExecutionContext, Future } +import scala.reflect.ClassTag + +/** + * A request header that only allows access if an identity is authenticated and authorized. + * + * @tparam E The type of the environment. + */ +trait SecuredRequestHeader[E <: Env] extends RequestHeader { + /** + * @return The identity implementation. + */ + def identity: I[E] + + /** + * @return The authenticator implementation. + */ + def authenticator: A[E] +} + +/** + * A request that only allows access if an identity is authenticated and authorized. + * + * @tparam E The type of the environment. + * @tparam B The type of the request body. + */ +trait SecuredRequest[E <: Env, +B] extends Request[B] with SecuredRequestHeader[E] + +object SecuredRequest { + /** + * A request that only allows access if an identity is authenticated and authorized. + * + * @param identity The identity implementation. + * @param authenticator The authenticator implementation. + * @param request The current request. + * @tparam E The type of the environment. + * @tparam B The type of the request body. + */ + def apply[E <: Env, B](identity: I[E], authenticator: A[E], request: Request[B]): SecuredRequest[E, B] = { + new DefaultSecuredRequest(identity, authenticator, request) + } + + /** + * Unapply method for secured request. + * + * @param securedRequest the secured request. + * @tparam E The type of the environment. + * @tparam B The type of the request body. + */ + @nowarn + def unapply[E <: Env, B](securedRequest: SecuredRequest[E, B]): Option[(I[E], A[E], Request[B])] = { + securedRequest match { + case dsr: DefaultSecuredRequest[E, B] => + Some((dsr.identity, dsr.authenticator, dsr.request)) + case sr: SecuredRequest[E, B] => + Some((sr.identity, sr.authenticator, sr)) + } + } +} + +class DefaultSecuredRequest[E <: Env, B]( + val identity: I[E], + val authenticator: A[E], + val request: Request[B]) extends WrappedRequest(request) with SecuredRequest[E, B] + +/** + * Request handler builder implementation to provide the foundation for secured request handlers. + * + * @param environment The environment instance to handle the request. + * @param errorHandler The instance of the secured error handler. + * @param authorization Maybe an authorization instance. + * @tparam E The type of the environment. + */ +final case class SecuredRequestHandlerBuilder[E <: Env]( + environment: Environment[E], + errorHandler: SecuredErrorHandler, + authorization: Option[Authorization[I[E], A[E]]]) + extends RequestHandlerBuilder[E, ({ type R[B] = SecuredRequest[E, B] })#R] { + + /** + * Creates a secured action handler builder with a new error handler in place. + * + * @param securedErrorHandler An error handler instance. + * @return A secured action handler builder with a new error handler in place. + */ + def apply(securedErrorHandler: SecuredErrorHandler): SecuredRequestHandlerBuilder[E] = + SecuredRequestHandlerBuilder[E](environment, securedErrorHandler, authorization) + + /** + * Creates a secured action handler builder with an authorization in place. + * + * @param specifiedAuthorization An authorization object that checks if the user is authorized to invoke the action. + * @return A secured action handler builder with an authorization in place. + */ + def apply(specifiedAuthorization: Authorization[I[E], A[E]]): SecuredRequestHandlerBuilder[E] = + SecuredRequestHandlerBuilder[E](environment, errorHandler, Some(specifiedAuthorization)) + + /** + * Invokes the block. + * + * @param block The block of code to invoke. + * @param request The current request. + * @tparam B The type of the request body. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + override def invokeBlock[B, T](block: SecuredRequest[E, B] => Future[HandlerResult[T]])( + implicit + request: Request[B]): Future[HandlerResult[T]] = { + withAuthorization(handleAuthentication).flatMap { + // A user is both authenticated and authorized. The request will be granted + case (Some(authenticator), Some(identity), Some(authorized)) if authorized => + environment.eventBus.publish(AuthenticatedEvent(identity, request)) + handleBlock(authenticator, a => block(SecuredRequest(identity, a, request))) + // A user is authenticated but not authorized. The request will be forbidden + case (Some(authenticator), Some(identity), _) => + environment.eventBus.publish(NotAuthorizedEvent(identity, request)) + handleBlock(authenticator, _ => errorHandler.onNotAuthorized.map(r => HandlerResult(r))) + // An authenticator but no user was found. The request will ask for authentication and the authenticator will be discarded + case (Some(authenticator), None, _) => + environment.eventBus.publish(NotAuthenticatedEvent(request)) + for { + result <- errorHandler.onNotAuthenticated + discardedResult <- environment.authenticatorService.discard(authenticator.extract, result) + } yield HandlerResult(discardedResult) + // No authenticator and no user was found. The request will ask for authentication + case _ => + environment.eventBus.publish(NotAuthenticatedEvent(request)) + errorHandler.onNotAuthenticated.map(r => HandlerResult(r)) + } + } + + /** + * Adds the authorization status to the authentication result. + * + * @param result The authentication result. + * @param request The current request. + * @tparam B The type of the request body. + * @return The authentication result with the additional authorization status. + */ + private def withAuthorization[B](result: Future[(Option[Either[A[E], A[E]]], Option[I[E]])])(implicit request: Request[B]) = { + result.flatMap { + case (Some(a), Some(i)) => + authorization.map(_.isAuthorized(i, a.extract)).getOrElse(Future.successful(true)).map(b => (Some(a), Some(i), Some(b))) + case (a, i) => + Future.successful((a, i, None)) + } + } +} + +/** + * A secured request handler. + * + * A handler which intercepts requests and checks if there is an authenticated user. + * If there is one, the execution continues and the enclosed code is invoked. + * + * If the user is not authenticated or not authorized, the request is forwarded to + * the [[io.github.honeycombcheesecake.play.silhouette.api.actions.SecuredErrorHandler.onNotAuthenticated]] or + * the [[io.github.honeycombcheesecake.play.silhouette.api.actions.SecuredErrorHandler.onNotAuthorized]] methods. + */ +trait SecuredRequestHandler { + + /** + * The instance of the secured error handler. + */ + val errorHandler: SecuredErrorHandler + + /** + * Applies the environment to the request handler stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A secured request handler builder. + */ + def apply[E <: Env](environment: Environment[E]): SecuredRequestHandlerBuilder[E] +} + +/** + * Default implementation of the [[SecuredRequestHandler]]. + * + * @param errorHandler The instance of the secured error handler. + */ +class DefaultSecuredRequestHandler @Inject() (val errorHandler: SecuredErrorHandler) + extends SecuredRequestHandler { + + /** + * Applies the environment to the request handler stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A secured request handler builder. + */ + override def apply[E <: Env](environment: Environment[E]): SecuredRequestHandlerBuilder[E] = + SecuredRequestHandlerBuilder[E](environment, errorHandler, None) +} + +/** + * Action builder implementation to provide the foundation for secured actions. + * + * @param requestHandler The request handler instance. + * @param parser The body parser. + * @tparam E The type of the environment. + * @tparam P The type of the request body. + */ +final case class SecuredActionBuilder[E <: Env, P]( + requestHandler: SecuredRequestHandlerBuilder[E], + parser: BodyParser[P]) extends ActionBuilder[({ type R[B] = SecuredRequest[E, B] })#R, P] { + + /** + * Creates a secured action builder with a new error handler in place. + * + * @param errorHandler An error handler instance. + * @return A secured action builder. + */ + def apply(errorHandler: SecuredErrorHandler): SecuredActionBuilder[E, P] = SecuredActionBuilder[E, P](requestHandler(errorHandler), parser) + + /** + * Creates a secured action builder with an authorization in place. + * + * @param authorization An authorization object that checks if the user is authorized to invoke the action. + * @return A secured action builder. + */ + def apply(authorization: Authorization[I[E], A[E]]): SecuredActionBuilder[E, P] = SecuredActionBuilder[E, P](requestHandler(authorization), parser) + + /** + * Invokes the block. + * + * @param request The current request. + * @param block The block of code to invoke. + * @tparam B The type of the request body. + * @return A handler result. + */ + override def invokeBlock[B](request: Request[B], block: SecuredRequest[E, B] => Future[Result]) = { + implicit val ec: ExecutionContext = executionContext + implicit val req: Request[B] = request + val b = (r: SecuredRequest[E, B]) => block(r).map(r => HandlerResult(r)) + + requestHandler(request)(b).map(_.result).recoverWith(requestHandler.errorHandler.exceptionHandler) + } + + /** + * Get the execution context to run the request in. + * + * @return The execution context. + */ + override protected def executionContext: ExecutionContext = requestHandler.executionContext +} + +/** + * An action based on the [[SecuredRequestHandler]]. + */ +trait SecuredAction { + + /** + * The instance of the secured request handler. + */ + val requestHandler: SecuredRequestHandler + + /** + * The default body parser. + */ + val bodyParser: BodyParsers.Default + + /** + * Applies the environment to the action stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A secured action builder. + */ + def apply[E <: Env](environment: Environment[E]): SecuredActionBuilder[E, AnyContent] +} + +/** + * Default implementation of the [[SecuredAction]]. + * + * @param requestHandler The instance of the secured request handler. + * @param bodyParser The default body parser. + */ +class DefaultSecuredAction @Inject() ( + val requestHandler: SecuredRequestHandler, + val bodyParser: BodyParsers.Default) extends SecuredAction { + + /** + * Applies the environment to the action stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A secured action builder. + */ + override def apply[E <: Env](environment: Environment[E]): SecuredActionBuilder[E, AnyContent] = + SecuredActionBuilder[E, AnyContent](requestHandler[E](environment), bodyParser) +} + +/** + * Error handler for secured actions. + */ +trait SecuredErrorHandler extends NotAuthenticatedErrorHandler with NotAuthorizedErrorHandler { + + /** + * Exception handler which chains the exceptions handlers from the sub types. + * + * @param request The request header. + * @return A partial function which maps an exception to a Play result. + */ + override def exceptionHandler(implicit request: RequestHeader): PartialFunction[Throwable, Future[Result]] = { + super[NotAuthenticatedErrorHandler].exceptionHandler orElse + super[NotAuthorizedErrorHandler].exceptionHandler + } +} + +/** + * Default implementation of the [[SecuredErrorHandler]]. + * + * @param messagesApi The Play messages API. + */ +class DefaultSecuredErrorHandler @Inject() (val messagesApi: MessagesApi) + extends SecuredErrorHandler + with DefaultNotAuthenticatedErrorHandler + with DefaultNotAuthorizedErrorHandler { + + /** + * Exception handler which chains the exceptions handlers from the sub types. + * + * @param request The request header. + * @return A partial function which maps an exception to a Play result. + */ + override def exceptionHandler(implicit request: RequestHeader): PartialFunction[Throwable, Future[Result]] = { + super[DefaultNotAuthenticatedErrorHandler].exceptionHandler orElse + super[DefaultNotAuthorizedErrorHandler].exceptionHandler + } +} + +/** + * Play module for providing the secured action components. + */ +class SecuredActionModule extends Module { + def bindings(environment: PlayEnv, configuration: Configuration) = { + Seq( + bind[SecuredAction].to[DefaultSecuredAction], + bind[SecuredRequestHandler].to[DefaultSecuredRequestHandler]) + } +} + +/** + * Play module to provide the secured error handler component. + * + * We provide an extra module so that it can be easily replaced with a custom implementation + * without to declare bindings for the other secured action module. + */ +class SecuredErrorHandlerModule extends Module { + def bindings(environment: PlayEnv, configuration: Configuration) = { + Seq( + bind[SecuredErrorHandler].to[DefaultSecuredErrorHandler]) + } +} + +/** + * Injection helper for secured action components + */ +trait SecuredActionComponents { + + def securedErrorHandler: SecuredErrorHandler + def securedBodyParser: BodyParsers.Default + + lazy val securedRequestHandler: SecuredRequestHandler = new DefaultSecuredRequestHandler(securedErrorHandler) + lazy val securedAction: SecuredAction = new DefaultSecuredAction(securedRequestHandler, securedBodyParser) +} + +/** + * Injection helper for secured error handler component. + * + * We provide an extra component so that it can be easily replaced with a custom implementation + * without to declare bindings for the other secured action component. + */ +trait SecuredErrorHandlerComponents { + + def messagesApi: MessagesApi + + lazy val securedErrorHandler: SecuredErrorHandler = new DefaultSecuredErrorHandler(messagesApi) +} diff --git a/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareAction.scala b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareAction.scala new file mode 100644 index 00000000..0d4783d7 --- /dev/null +++ b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareAction.scala @@ -0,0 +1,261 @@ +/** + * Original work: SecureSocial (https://github.com/jaliss/securesocial) + * Copyright 2013 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss + * + * Derivative work: Silhouette (https://github.com/mohiva/play-silhouette) + * Modifications Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette.api.actions + +import javax.inject.Inject + +import io.github.honeycombcheesecake.play.silhouette.api._ +import play.api.{ Configuration, Environment => PlayEnv } +import play.api.inject.Module +import play.api.mvc._ + +import scala.annotation.nowarn +import scala.concurrent.{ ExecutionContext, Future } + +/** + * A request that adds maybe the identity and maybe the authenticator for the current call. + * + * @tparam E The type of the environment. + */ +trait UserAwareRequestHeader[E <: Env] extends RequestHeader { + /** + * @return Some identity implementation if authentication was successful, None otherwise. + */ + def identity: Option[I[E]] + + /** + * @return Some authenticator implementation if authentication was successful, None otherwise. + */ + def authenticator: Option[A[E]] +} + +trait UserAwareRequest[E <: Env, +B] extends Request[B] with UserAwareRequestHeader[E] + +object UserAwareRequest { + + /** + * A request that adds maybe the identity and maybe the authenticator for the current call. + * + * @param identity Some identity implementation if authentication was successful, None otherwise. + * @param authenticator Some authenticator implementation if authentication was successful, None otherwise. + * @param request The current request. + * @tparam E The type of the environment. + * @tparam B The type of the request body. + */ + def apply[E <: Env, B]( + identity: Option[I[E]], + authenticator: Option[A[E]], + request: Request[B]): UserAwareRequest[E, B] = { + new DefaultUserAwareRequest(identity, authenticator, request) + } + + /** + * Unapply method for user aware request. + * + * @param userAwareRequest the user aware request. + * @tparam E The type of the environment. + * @tparam B The type of the request body. + */ + @nowarn + def unapply[E <: Env, B](userAwareRequest: UserAwareRequest[E, B]): Option[(Option[I[E]], Option[A[E]], Request[B])] = { + userAwareRequest match { + case duar: DefaultUserAwareRequest[E, B] => + Some((duar.identity, duar.authenticator, duar.request)) + case uar: UserAwareRequest[E, B] => + Some((uar.identity, uar.authenticator, uar)) + } + } +} + +class DefaultUserAwareRequest[E <: Env, B]( + val identity: Option[I[E]], + val authenticator: Option[A[E]], + val request: Request[B]) extends WrappedRequest(request) with UserAwareRequest[E, B] + +/** + * Request handler builder implementation to provide the foundation for user-aware request handlers. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + */ +final case class UserAwareRequestHandlerBuilder[E <: Env](environment: Environment[E]) + extends RequestHandlerBuilder[E, ({ type R[B] = UserAwareRequest[E, B] })#R] { + + /** + * Invokes the block. + * + * @param block The block of code to invoke. + * @param request The current request. + * @tparam B The type of the request body. + * @tparam T The type of the data included in the handler result. + * @return A handler result. + */ + override def invokeBlock[B, T](block: UserAwareRequest[E, B] => Future[HandlerResult[T]])(implicit request: Request[B]): Future[HandlerResult[T]] = { + handleAuthentication.flatMap { + // A valid authenticator was found and the identity may be exists + case (Some(authenticator), identity) if authenticator.extract.isValid => + handleBlock(authenticator, a => block(UserAwareRequest(identity, Some(a), request))) + // An invalid authenticator was found. The authenticator will be discarded + case (Some(authenticator), _) if !authenticator.extract.isValid => + block(UserAwareRequest(None, None, request)).flatMap { + case hr @ HandlerResult(pr, _) => + environment.authenticatorService.discard(authenticator.extract, pr).map(r => hr.copy(r)) + } + // No authenticator and no user was found + case _ => block(UserAwareRequest(None, None, request)) + } + } +} + +/** + * A user-aware request handler. + * + * A handler that can be used for endpoints that need to know if there is a current user but + * can be executed even if there isn't one. + */ +trait UserAwareRequestHandler { + + /** + * Applies the environment to the request handler stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A user-aware request handler builder. + */ + def apply[E <: Env](environment: Environment[E]): UserAwareRequestHandlerBuilder[E] +} + +/** + * Default implementation of the [[UserAwareRequestHandler]]. + */ +class DefaultUserAwareRequestHandler extends UserAwareRequestHandler { + + /** + * Applies the environment to the request handler stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A user-aware request handler builder. + */ + override def apply[E <: Env](environment: Environment[E]): UserAwareRequestHandlerBuilder[E] = UserAwareRequestHandlerBuilder[E](environment) +} + +/** + * Action builder implementation to provide the foundation for user-aware actions. + * + * @param requestHandler The request handler instance. + * @param parser The body parser. + * @tparam E The type of the environment. + * @tparam P The type of the request body. + */ +final case class UserAwareActionBuilder[E <: Env, P]( + requestHandler: UserAwareRequestHandlerBuilder[E], + parser: BodyParser[P]) extends ActionBuilder[({ type R[B] = UserAwareRequest[E, B] })#R, P] { + + /** + * Invokes the block. + * + * @param request The current request. + * @param block The block of code to invoke. + * @tparam B The type of the request body. + * @return The result to send to the client. + */ + override def invokeBlock[B](request: Request[B], block: UserAwareRequest[E, B] => Future[Result]) = { + implicit val ec: ExecutionContext = executionContext + requestHandler(request) { req => + block(req).map(r => HandlerResult(r)) + }.map(_.result) + } + + /** + * Get the execution context to run the request in. + * + * @return The execution context. + */ + override protected def executionContext: ExecutionContext = requestHandler.executionContext +} + +/** + * An action based on the [[UserAwareRequestHandler]]. + */ +trait UserAwareAction { + + /** + * The instance of the user-aware request handler. + */ + val requestHandler: UserAwareRequestHandler + + /** + * The default body parser. + */ + val bodyParser: BodyParsers.Default + + /** + * Applies the environment to the action stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A user-aware action builder. + */ + def apply[E <: Env](environment: Environment[E]): UserAwareActionBuilder[E, AnyContent] +} + +/** + * Default implementation of the [[UserAwareAction]]. + * + * @param requestHandler The instance of the user-aware request handler. + * @param bodyParser The default body parser. + */ +class DefaultUserAwareAction @Inject() ( + val requestHandler: UserAwareRequestHandler, + val bodyParser: BodyParsers.Default) extends UserAwareAction { + + /** + * Applies the environment to the action stack. + * + * @param environment The environment instance to handle the request. + * @tparam E The type of the environment. + * @return A user-aware action builder. + */ + override def apply[E <: Env](environment: Environment[E]): UserAwareActionBuilder[E, AnyContent] = + UserAwareActionBuilder[E, AnyContent](requestHandler[E](environment), bodyParser) +} + +/** + * Play module for providing the user-aware action components. + */ +class UserAwareActionModule extends Module { + def bindings(environment: PlayEnv, configuration: Configuration) = { + Seq( + bind[UserAwareAction].to[DefaultUserAwareAction], + bind[UserAwareRequestHandler].to[DefaultUserAwareRequestHandler]) + } +} + +/** + * Injection helper for user-aware action components + */ +trait UserAwareActionComponents { + + def userAwareBodyParser: BodyParsers.Default + + lazy val userAwareRequestHandler: UserAwareRequestHandler = new DefaultUserAwareRequestHandler() + lazy val userAwareAction: UserAwareAction = new DefaultUserAwareAction(userAwareRequestHandler, userAwareBodyParser) +} diff --git a/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorService.scala b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorService.scala new file mode 100644 index 00000000..8ab07dcb --- /dev/null +++ b/silhouette/app-3/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorService.scala @@ -0,0 +1,226 @@ +/** + * Original work: SecureSocial (https://github.com/jaliss/securesocial) + * Copyright 2013 Jorge Aliss (jaliss at gmail dot com) - twitter: @jaliss + * + * Derivative work: Silhouette (https://github.com/mohiva/play-silhouette) + * Modifications Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.honeycombcheesecake.play.silhouette.api.services + +import io.github.honeycombcheesecake.play.silhouette.api.util.{ ExtractableRequest, ExecutionContextProvider } +import io.github.honeycombcheesecake.play.silhouette.api.{ Authenticator, LoginInfo, Value } +import play.api.http.HttpEntity +import play.api.libs.typedmap.TypedMap +import play.api.mvc._ + +import scala.concurrent.Future + +/** + * A marker result which indicates that an operation on an authenticator was processed and + * therefore it shouldn't updated automatically. + * + * Due the fact that the update method gets called on every subsequent request to update the + * authenticator related data in the backing store and in the result, it isn't possible to + * discard or renew the authenticator simultaneously. This is because the "update" method would + * override the result created by the "renew" or "discard" method, because it will be executed + * as last in the chain. + * + * As example: + * If we discard the session in a Silhouette action then it will be removed from session. But + * at the end the update method will embed the session again, because it gets called with the + * result of the action. + * + * @param result The result to wrap. + */ +class AuthenticatorResult(result: Result) + extends Result(result.header, result.body, result.newSession, result.newFlash, result.newCookies, result.attrs) { + + /** + * Creates a new copy of a `AuthenticatorResult`. + * + * @param header The response header, which contains status code and HTTP headers. + * @param body The response body. + * @param newSession A new session. + * @param newFlash A new flash. + * @param newCookies Some new cookies. + * @return A copy of a `AuthenticatorResult`. + */ + override def copy( + header: ResponseHeader, + body: HttpEntity, + newSession: Option[Session], + newFlash: Option[Flash], + newCookies: Seq[Cookie], + attrs: TypedMap) = { + AuthenticatorResult(super.copy(header, body, newSession, newFlash, newCookies, attrs)) + } +} + +/** + * The companion object. + */ +object AuthenticatorResult { + + /** + * Instantiates a new authenticator result. + * + * @param result The result to wrap. + * @return An authenticator result. + */ + def apply(result: Result) = new AuthenticatorResult(result) +} + +/** + * Handles authenticators for the Silhouette module. + * + * @tparam T The type of the authenticator this service is responsible for. + */ +trait AuthenticatorService[T <: Authenticator] extends ExecutionContextProvider { + + /** + * Creates a new authenticator for the specified login info. + * + * @param loginInfo The login info for which the authenticator should be created. + * @param request The request header. + * @return An authenticator. + */ + def create(loginInfo: LoginInfo)(implicit request: RequestHeader): Future[T] + + /** + * Retrieves the authenticator from request. + * + * @param request The request to retrieve the authenticator from. + * @tparam B The type of the request body. + * @return Some authenticator or None if no authenticator could be found in request. + */ + def retrieve[B](implicit request: ExtractableRequest[B]): Future[Option[T]] + + /** + * Initializes an authenticator and instead of embedding into the the request or result, it returns + * the serialized value. + * + * @param authenticator The authenticator instance. + * @param request The request header. + * @return The serialized authenticator value. + */ + def init(authenticator: T)(implicit request: RequestHeader): Future[Value[T]] + + /** + * Embeds authenticator specific artifacts into the response. + * + * @param value The authenticator value to embed. + * @param result The result to manipulate. + * @param request The request header. + * @return The manipulated result. + */ + def embed(value: Value[T], result: Result)(implicit request: RequestHeader): Future[AuthenticatorResult] + + /** + * Embeds authenticator specific artifacts into the request. + * + * This method can be used to embed an authenticator in a existing request. This can be useful + * in Play filters. So before executing a SecuredAction we can embed the authenticator in + * the request to lead the action to believe that the request is a new request which contains + * a valid authenticator. + * + * If an existing authenticator exists, then it will be overridden. + * + * @param value The authenticator value to embed. + * @param request The request header. + * @return The manipulated request header. + */ + def embed(value: Value[T], request: RequestHeader): RequestHeader + + /** + * Touches an authenticator. + * + * An authenticator can use sliding window expiration. This means that the authenticator times + * out after a certain time if it wasn't used. So to mark an authenticator as used it will be + * touched on every request to a Silhouette action. If an authenticator should not be touched + * because of the fact that sliding window expiration is disabled, then it should be returned + * on the right, otherwise it should be returned on the left. An untouched authenticator needn't + * be updated later by the [[update]] method. + * + * @param authenticator The authenticator to touch. + * @return The touched authenticator on the left or the untouched authenticator on the right. + */ + def touch(authenticator: T): Either[T, T] + + /** + * Updates a touched authenticator. + * + * If the authenticator was updated, then the updated artifacts should be embedded into the response. + * This method gets called on every subsequent request if an identity accesses a Silhouette action, + * expect the authenticator was not touched. + * + * @param authenticator The authenticator to update. + * @param result The result to manipulate. + * @param request The request header. + * @return The original or a manipulated result. + */ + def update(authenticator: T, result: Result)(implicit request: RequestHeader): Future[AuthenticatorResult] + + /** + * Renews the expiration of an authenticator without embedding it into the result. + * + * Based on the implementation, the renew method should revoke the given authenticator first, before + * creating a new one. If the authenticator was updated, then the updated artifacts should be returned. + * + * @param authenticator The authenticator to renew. + * @param request The request header. + * @return The serialized expression of the authenticator. + */ + def renew(authenticator: T)(implicit request: RequestHeader): Future[Value[T]] + + /** + * Renews the expiration of an authenticator. + * + * Based on the implementation, the renew method should revoke the given authenticator first, before + * creating a new one. If the authenticator was updated, then the updated artifacts should be embedded + * into the response. + * + * @param authenticator The authenticator to renew. + * @param result The result to manipulate. + * @param request The request header. + * @return The original or a manipulated result. + */ + def renew(authenticator: T, result: Result)(implicit request: RequestHeader): Future[AuthenticatorResult] + + /** + * Manipulates the response and removes authenticator specific artifacts before sending it to the client. + * + * @param authenticator The authenticator instance. + * @param result The result to manipulate. + * @param request The request header. + * @return The manipulated result. + */ + def discard(authenticator: T, result: Result)(implicit request: RequestHeader): Future[AuthenticatorResult] +} + +/** + * The companion object. + */ +object AuthenticatorService { + + /** + * The error messages. + */ + val CreateError = "[Silhouette][%s] Could not create authenticator for login info: %s" + val RetrieveError = "[Silhouette][%s] Could not retrieve authenticator" + val InitError = "[Silhouette][%s] Could not init authenticator: %s" + val UpdateError = "[Silhouette][%s] Could not update authenticator: %s" + val RenewError = "[Silhouette][%s] Could not renew authenticator: %s" + val DiscardError = "[Silhouette][%s] Could not discard authenticator: %s" +} diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticator.scala b/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticator.scala index e3786b53..a3effdd0 100644 --- a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticator.scala +++ b/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticator.scala @@ -30,10 +30,12 @@ import io.github.honeycombcheesecake.play.silhouette.api.util._ import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.CookieAuthenticatorService._ import play.api.libs.json.{ Json, OFormat } import play.api.mvc._ +import play.api.mvc.{ Cookie, CookieHeaderEncoding } import play.api.mvc.request.{ Cell, RequestAttrKey } import java.time.ZonedDateTime import scala.concurrent.duration._ +import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ ExecutionContext, Future } import scala.util.{ Failure, Success, Try } @@ -280,7 +282,7 @@ class CookieAuthenticatorService( val combinedCookies = filteredCookies :+ cookie val cookies = Cookies(combinedCookies) - request.withAttrs(request.attrs.updated(RequestAttrKey.Cookies.bindValue(Cell(cookies)))) + request.withAttrs(request.attrs.updated(RequestAttrKey.Cookies, Cell(cookies))) } /** diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticator.scala b/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticator.scala index 7112179d..92cd5084 100644 --- a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticator.scala +++ b/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticator.scala @@ -25,10 +25,12 @@ import io.github.honeycombcheesecake.play.silhouette.api.{ Authenticator, Expira import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.SessionAuthenticatorService._ import play.api.libs.json.{ Json, OFormat } import play.api.mvc._ +import play.api.mvc.SessionCookieBaker import play.api.mvc.request.{ Cell, RequestAttrKey } import java.time.ZonedDateTime import scala.concurrent.duration._ +import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ ExecutionContext, Future } import scala.util.{ Failure, Success, Try } @@ -214,7 +216,7 @@ class SessionAuthenticatorService( case None => session } - request.withAttrs(request.attrs.updated(RequestAttrKey.Session.bindValue(Cell(s)))) + request.withAttrs(request.attrs.updated(RequestAttrKey.Session, Cell(s))) } /** diff --git a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2Provider.scala b/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2Provider.scala index 92b54e6a..863abc40 100644 --- a/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2Provider.scala +++ b/silhouette/app/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2Provider.scala @@ -29,6 +29,7 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.OAuth2Provid import io.github.honeycombcheesecake.play.silhouette.impl.providers.state.UserStateItemHandler import play.api.libs.functional.syntax._ import play.api.libs.json._ +import play.api.libs.ws.DefaultBodyWritables.writeableOf_urlEncodedForm import play.api.libs.ws.WSResponse import play.api.mvc._ diff --git a/silhouette/test/Helpers.scala b/silhouette/test/Helpers.scala index 0684f6f6..da261fd7 100644 --- a/silhouette/test/Helpers.scala +++ b/silhouette/test/Helpers.scala @@ -19,8 +19,6 @@ import io.github.honeycombcheesecake.play.silhouette.api.AuthInfo import io.github.honeycombcheesecake.play.silhouette.impl.providers.{ SocialProfile, SocialStateItem, StatefulAuthInfo } import org.specs2.execute.{ AsResult, Result => Specs2Result } import org.specs2.matcher.{ JsonMatchers, MatchResult } -import org.specs2.mock.Mockito -import org.specs2.mutable.Around import play.api.libs.json.{ JsValue, Json } import play.api.mvc.{ Result => PlayResult } import play.api.test.PlaySpecification @@ -28,42 +26,12 @@ import play.api.test.PlaySpecification import scala.concurrent.Future import scala.io.{ Codec, Source } import scala.reflect.ClassTag - -/** - * Executes a before method in the context of the around method. - */ -trait BeforeWithinAround extends Around { - def before: Any - abstract override def around[T: AsResult](t: => T): Specs2Result = super.around { - before; t - } -} - -/** - * Executes an after method in the context of the around method. - */ -trait AfterWithinAround extends Around { - def after: Any - abstract override def around[T: AsResult](t: => T): Specs2Result = super.around { - try { t } finally { after } - } -} - -/** - * Executes before and after methods in the context of the around method. - */ -trait BeforeAfterWithinAround extends Around { - def before: Any - def after: Any - abstract override def around[T: AsResult](t: => T): Specs2Result = super.around { - try { before; t } finally { after } - } -} +import org.mockito.Mockito /** * Base test case for the social providers. */ -trait SocialProviderSpec[A <: AuthInfo] extends PlaySpecification with Mockito with JsonMatchers { +trait SocialProviderSpec[A <: AuthInfo] extends PlaySpecification with JsonMatchers { /** * Applies a matcher on a simple result. @@ -121,7 +89,7 @@ trait SocialProviderSpec[A <: AuthInfo] extends PlaySpecification with Mockito w lazy val result = await(providerResult.failed) - result must not[Any](throwAn[E]) + result must not[Throwable](throwAn[E]) result.rethrow must throwAn[E].like(f) } } @@ -177,4 +145,15 @@ object Helper { case None => throw new Exception("Cannot load file: " + file) } } + + /** + * Mock related helpers + */ + + def mock[A](implicit a: ClassTag[A]): A = + Mockito.mock(a.runtimeClass).asInstanceOf[A] + + def mockSmart[A](implicit a: ClassTag[A]): A = + Mockito.mock(a.runtimeClass, Mockito.withSettings().defaultAnswer(Mockito.RETURNS_SMART_NULLS)).asInstanceOf[A] + } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/ErrorHandlerSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/ErrorHandlerSpec.scala index 4b577b95..f90b48d1 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/ErrorHandlerSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/ErrorHandlerSpec.scala @@ -30,125 +30,173 @@ class ErrorHandlerSpec extends PlaySpecification { "The `DefaultNotAuthenticatedErrorHandler.notAuthenticated` method" should { "return an HTML response for an HTML request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(HTML), - expectedStatus = UNAUTHORIZED, - expectedContentType = HTML, - expectedResponseFragment = "", - expectedMessage = "silhouette.not.authenticated", - f = { r: RequestHeader => notAuthenticated.onNotAuthenticated(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(HTML), + expectedStatus = UNAUTHORIZED, + expectedContentType = HTML, + expectedResponseFragment = "", + expectedMessage = "silhouette.not.authenticated", + f = { + notAuthenticated.onNotAuthenticated(_: RequestHeader) + }) + } } "return a JSON response for a JSON request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(JSON), - expectedStatus = UNAUTHORIZED, - expectedContentType = JSON, - expectedResponseFragment = "\"success\":false", - expectedMessage = "silhouette.not.authenticated", - f = { r: RequestHeader => notAuthenticated.onNotAuthenticated(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(JSON), + expectedStatus = UNAUTHORIZED, + expectedContentType = JSON, + expectedResponseFragment = "\"success\":false", + expectedMessage = "silhouette.not.authenticated", + f = { + notAuthenticated.onNotAuthenticated(_: RequestHeader) + }) + } } "return a XML response for a XML request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(XML), - expectedStatus = UNAUTHORIZED, - expectedContentType = XML, - expectedResponseFragment = "false", - expectedMessage = "silhouette.not.authenticated", - f = { r: RequestHeader => notAuthenticated.onNotAuthenticated(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(XML), + expectedStatus = UNAUTHORIZED, + expectedContentType = XML, + expectedResponseFragment = "false", + expectedMessage = "silhouette.not.authenticated", + f = { + notAuthenticated.onNotAuthenticated(_: RequestHeader) + }) + } } "return a plain text response for a plain text request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(TEXT), - expectedStatus = UNAUTHORIZED, - expectedContentType = TEXT, - expectedResponseFragment = messagesApi("silhouette.not.authenticated"), - expectedMessage = "silhouette.not.authenticated", - f = { r: RequestHeader => notAuthenticated.onNotAuthenticated(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(TEXT), + expectedStatus = UNAUTHORIZED, + expectedContentType = TEXT, + expectedResponseFragment = messagesApi("silhouette.not.authenticated"), + expectedMessage = "silhouette.not.authenticated", + f = { + notAuthenticated.onNotAuthenticated(_: RequestHeader) + }) + } } "return a plain text response for other requests" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(BINARY), - expectedStatus = UNAUTHORIZED, - expectedContentType = TEXT, - expectedResponseFragment = messagesApi("silhouette.not.authenticated"), - expectedMessage = "silhouette.not.authenticated", - f = { r: RequestHeader => notAuthenticated.onNotAuthenticated(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(BINARY), + expectedStatus = UNAUTHORIZED, + expectedContentType = TEXT, + expectedResponseFragment = messagesApi("silhouette.not.authenticated"), + expectedMessage = "silhouette.not.authenticated", + f = { + notAuthenticated.onNotAuthenticated(_: RequestHeader) + }) + } } "return an HTML response for a request without an Accept header" in new WithApplication with Context { - testResponse( - acceptedMediaType = None, - expectedStatus = UNAUTHORIZED, - expectedContentType = HTML, - expectedResponseFragment = messagesApi("silhouette.not.authenticated"), - expectedMessage = "silhouette.not.authenticated", - f = { r: RequestHeader => notAuthenticated.onNotAuthenticated(r) }) + override def running() = { + testResponse( + acceptedMediaType = None, + expectedStatus = UNAUTHORIZED, + expectedContentType = HTML, + expectedResponseFragment = messagesApi("silhouette.not.authenticated"), + expectedMessage = "silhouette.not.authenticated", + f = { + notAuthenticated.onNotAuthenticated(_: RequestHeader) + }) + } } } "The `DefaultNotAuthorizedErrorHandler.onNotAuthorized` method" should { "return an HTML response for an HTML request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(HTML), - expectedStatus = FORBIDDEN, - expectedContentType = HTML, - expectedResponseFragment = "", - expectedMessage = "silhouette.not.authorized", - f = { r: RequestHeader => notAuthorized.onNotAuthorized(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(HTML), + expectedStatus = FORBIDDEN, + expectedContentType = HTML, + expectedResponseFragment = "", + expectedMessage = "silhouette.not.authorized", + f = { + notAuthorized.onNotAuthorized(_: RequestHeader) + }) + } } "return a JSON response for a JSON request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(JSON), - expectedStatus = FORBIDDEN, - expectedContentType = JSON, - expectedResponseFragment = "\"success\":false", - expectedMessage = "silhouette.not.authorized", - f = { r: RequestHeader => notAuthorized.onNotAuthorized(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(JSON), + expectedStatus = FORBIDDEN, + expectedContentType = JSON, + expectedResponseFragment = "\"success\":false", + expectedMessage = "silhouette.not.authorized", + f = { + notAuthorized.onNotAuthorized(_: RequestHeader) + }) + } } "return a XML response for a XML request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(XML), - expectedStatus = FORBIDDEN, - expectedContentType = XML, - expectedResponseFragment = "false", - expectedMessage = "silhouette.not.authorized", - f = { r: RequestHeader => notAuthorized.onNotAuthorized(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(XML), + expectedStatus = FORBIDDEN, + expectedContentType = XML, + expectedResponseFragment = "false", + expectedMessage = "silhouette.not.authorized", + f = { + notAuthorized.onNotAuthorized(_: RequestHeader) + }) + } } "return a plain text response for a plain text request" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(TEXT), - expectedStatus = FORBIDDEN, - expectedContentType = TEXT, - expectedResponseFragment = messagesApi("silhouette.not.authorized"), - expectedMessage = "silhouette.not.authorized", - f = { r: RequestHeader => notAuthorized.onNotAuthorized(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(TEXT), + expectedStatus = FORBIDDEN, + expectedContentType = TEXT, + expectedResponseFragment = messagesApi("silhouette.not.authorized"), + expectedMessage = "silhouette.not.authorized", + f = { + notAuthorized.onNotAuthorized(_: RequestHeader) + }) + } } "return a plain text response for other requests" in new WithApplication with Context { - testResponse( - acceptedMediaType = Some(BINARY), - expectedStatus = FORBIDDEN, - expectedContentType = TEXT, - expectedResponseFragment = messagesApi("silhouette.not.authorized"), - expectedMessage = "silhouette.not.authorized", - f = { r: RequestHeader => notAuthorized.onNotAuthorized(r) }) + override def running() = { + testResponse( + acceptedMediaType = Some(BINARY), + expectedStatus = FORBIDDEN, + expectedContentType = TEXT, + expectedResponseFragment = messagesApi("silhouette.not.authorized"), + expectedMessage = "silhouette.not.authorized", + f = { + notAuthorized.onNotAuthorized(_: RequestHeader) + }) + } } "return an HTML response for a request without an Accept header" in new WithApplication with Context { - testResponse( - acceptedMediaType = None, - expectedStatus = FORBIDDEN, - expectedContentType = HTML, - expectedResponseFragment = messagesApi("silhouette.not.authorized"), - expectedMessage = "silhouette.not.authorized", - f = { r: RequestHeader => notAuthorized.onNotAuthorized(r) }) + override def running() = { + testResponse( + acceptedMediaType = None, + expectedStatus = FORBIDDEN, + expectedContentType = HTML, + expectedResponseFragment = messagesApi("silhouette.not.authorized"), + expectedMessage = "silhouette.not.authorized", + f = { + notAuthorized.onNotAuthorized(_: RequestHeader) + }) + } } } @@ -193,7 +241,7 @@ class ErrorHandlerSpec extends PlaySpecification { expectedMessage: String, f: RequestHeader => Future[Result]) = { implicit val request = acceptedMediaType match { - case Some(mediaType) => FakeRequest().withHeaders(ACCEPT -> mediaType) + case Some(mediaType) => FakeRequest().withHeaders((ACCEPT, mediaType)) case None => FakeRequest() } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/EventBusSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/EventBusSpec.scala index e7109774..a5e244e3 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/EventBusSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/EventBusSpec.scala @@ -17,7 +17,6 @@ package io.github.honeycombcheesecake.play.silhouette.api import akka.actor.{ Actor, ActorSystem, Props } import akka.testkit.TestProbe -import org.specs2.control.NoLanguageFeatures import org.specs2.specification.Scope import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } @@ -27,106 +26,120 @@ import scala.language.postfixOps /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.api.EventBus]] class. */ -class EventBusSpec extends PlaySpecification with NoLanguageFeatures { +class EventBusSpec extends PlaySpecification { "The event bus" should { "handle an subclass event" in new WithApplication with Context { - val eventBus = new EventBus - val listener = system.actorOf(Props(new Actor { - def receive = { - case e => theProbe.ref ! e - } - })) - - eventBus.subscribe(listener, classOf[SilhouetteEvent]) - - eventBus.publish(loginEvent) - theProbe.expectMsg(500 millis, loginEvent) - - eventBus.publish(logoutEvent) - theProbe.expectMsg(500 millis, logoutEvent) + override def running() = { + val eventBus = new EventBus + val listener = system.actorOf(Props(new Actor { + def receive = { + case e => theProbe.ref ! e + } + })) + + eventBus.subscribe(listener, classOf[SilhouetteEvent]) + + eventBus.publish(loginEvent) + theProbe.expectMsg(500 millis, loginEvent) + + eventBus.publish(logoutEvent) + theProbe.expectMsg(500 millis, logoutEvent) + } } "handle an event" in new WithApplication with Context { - val eventBus = new EventBus - val listener = system.actorOf(Props(new Actor { - def receive = { - case e @ LoginEvent(_, _) => theProbe.ref ! e - } - })) - - eventBus.subscribe(listener, classOf[LoginEvent[TestIdentity]]) - eventBus.publish(loginEvent) - - theProbe.expectMsg(500 millis, loginEvent) + override def running() = { + val eventBus = new EventBus + val listener = system.actorOf(Props(new Actor { + def receive = { + case e@LoginEvent(_, _) => theProbe.ref ! e + } + })) + + eventBus.subscribe(listener, classOf[LoginEvent[TestIdentity]]) + eventBus.publish(loginEvent) + + theProbe.expectMsg(500 millis, loginEvent) + } } "handle multiple events" in new WithApplication with Context { - val eventBus = new EventBus - val listener = system.actorOf(Props(new Actor { - def receive = { - case e @ LoginEvent(_, _) => theProbe.ref ! e - case e @ LogoutEvent(_, _) => theProbe.ref ! e - } - })) - - eventBus.subscribe(listener, classOf[LoginEvent[TestIdentity]]) - eventBus.subscribe(listener, classOf[LogoutEvent[TestIdentity]]) - eventBus.publish(loginEvent) - eventBus.publish(logoutEvent) - - theProbe.expectMsg(500 millis, loginEvent) - theProbe.expectMsg(500 millis, logoutEvent) + override def running() = { + val eventBus = new EventBus + val listener = system.actorOf(Props(new Actor { + def receive = { + case e@LoginEvent(_, _) => theProbe.ref ! e + case e@LogoutEvent(_, _) => theProbe.ref ! e + } + })) + + eventBus.subscribe(listener, classOf[LoginEvent[TestIdentity]]) + eventBus.subscribe(listener, classOf[LogoutEvent[TestIdentity]]) + eventBus.publish(loginEvent) + eventBus.publish(logoutEvent) + + theProbe.expectMsg(500 millis, loginEvent) + theProbe.expectMsg(500 millis, logoutEvent) + } } "differentiate between event classes" in new WithApplication with Context { - val eventBus = new EventBus - val listener = system.actorOf(Props(new Actor { - def receive = { - case e @ LoginEvent(_, _) => theProbe.ref ! e - } - })) - - eventBus.subscribe(listener, classOf[LogoutEvent[TestIdentity]]) - eventBus.publish(logoutEvent) - - theProbe.expectNoMessage(500 millis) + override def running() = { + val eventBus = new EventBus + val listener = system.actorOf(Props(new Actor { + def receive = { + case e@LoginEvent(_, _) => theProbe.ref ! e + } + })) + + eventBus.subscribe(listener, classOf[LogoutEvent[TestIdentity]]) + eventBus.publish(logoutEvent) + + theProbe.expectNoMessage(500 millis) + } } "not handle not subscribed events" in new WithApplication with Context { - val eventBus = new EventBus - val listener = system.actorOf(Props(new Actor { - def receive = { - case e @ LoginEvent(_, _) => theProbe.ref ! e - } - })) - - eventBus.publish(loginEvent) - - theProbe.expectNoMessage(500 millis) + override def running() = { + val eventBus = new EventBus + val listener = system.actorOf(Props(new Actor { + def receive = { + case e@LoginEvent(_, _) => theProbe.ref ! e + } + })) + + eventBus.publish(loginEvent) + + theProbe.expectNoMessage(500 millis) + } } "not handle events between different event buses" in new WithApplication with Context { - val eventBus1 = new EventBus - val eventBus2 = new EventBus + override def running() = { + val eventBus1 = new EventBus + val eventBus2 = new EventBus - val listener = system.actorOf(Props(new Actor { - def receive = { - case e @ LoginEvent(_, _) => theProbe.ref ! e - } - })) + val listener = system.actorOf(Props(new Actor { + def receive = { + case e@LoginEvent(_, _) => theProbe.ref ! e + } + })) - eventBus1.subscribe(listener, classOf[LoginEvent[TestIdentity]]) - eventBus2.publish(loginEvent) + eventBus1.subscribe(listener, classOf[LoginEvent[TestIdentity]]) + eventBus2.publish(loginEvent) - theProbe.expectNoMessage(500 millis) + theProbe.expectNoMessage(500 millis) + } } "returns a singleton event bus" in new WithApplication with Context { - val eventBus1 = EventBus() - val eventBus2 = EventBus() + override def running() = { + val eventBus1 = EventBus() + val eventBus2 = EventBus() - eventBus1 ==== eventBus2 + eventBus1 ==== eventBus2 + } } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredActionSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredActionSpec.scala index 4864c218..a55d5640 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredActionSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/SecuredActionSpec.scala @@ -15,6 +15,8 @@ */ package io.github.honeycombcheesecake.play.silhouette.api.actions +import com.google.inject.AbstractModule + import javax.inject.Inject import akka.actor.{ Actor, ActorSystem, Props } @@ -24,16 +26,18 @@ import io.github.honeycombcheesecake.play.silhouette.api.actions.SecuredActionSp import io.github.honeycombcheesecake.play.silhouette.api.exceptions.{ NotAuthenticatedException, NotAuthorizedException } import io.github.honeycombcheesecake.play.silhouette.api.services.{ AuthenticatorResult, AuthenticatorService, IdentityService } import net.codingwell.scalaguice.ScalaModule -import org.specs2.control.NoLanguageFeatures import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder +import play.api.inject.guice.GuiceableModule import play.api.libs.json.Json import play.api.mvc.Results._ import play.api.mvc._ import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ +import test.Helper.mock import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -44,403 +48,443 @@ import scala.reflect.ClassTag /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.api.actions.SecuredActionSpec]]. */ -class SecuredActionSpec extends PlaySpecification with Mockito with JsonMatchers with NoLanguageFeatures { +class SecuredActionSpec extends PlaySpecification with JsonMatchers { "The `SecuredAction` action" should { "restrict access if no valid authenticator can be retrieved" in new InjectorContext { new WithApplication(app) with Context { - withEvent[NotAuthenticatedEvent] { - env.authenticatorService.retrieve(any()) returns Future.successful(None) + override def running() = { + withEvent[NotAuthenticatedEvent] { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(UNAUTHORIZED) - contentAsString(result) must contain("global.not.authenticated") - theProbe.expectMsg(500 millis, NotAuthenticatedEvent(request)) + status(result) must equalTo(UNAUTHORIZED) + contentAsString(result) must contain("global.not.authenticated") + theProbe.expectMsg(500 millis, NotAuthenticatedEvent(request)) + } } } } "restrict access and discard authenticator if an invalid authenticator can be retrieved" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator.copy(isValid = false))) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator.copy(isValid = false)))) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } - withEvent[NotAuthenticatedEvent] { - val result = controller.defaultAction(request) + withEvent[NotAuthenticatedEvent] { + val result = controller.defaultAction(request) - status(result) must equalTo(UNAUTHORIZED) - contentAsString(result) must contain("global.not.authenticated") - there was one(env.authenticatorService).discard(any(), any())(any()) - theProbe.expectMsg(500 millis, NotAuthenticatedEvent(request)) + status(result) must equalTo(UNAUTHORIZED) + contentAsString(result) must contain("global.not.authenticated") + verify(env.authenticatorService).discard(any(), any())(any()) + theProbe.expectMsg(500 millis, NotAuthenticatedEvent(request)) + } } } } "restrict access and discard authenticator if no identity could be found for an authenticator" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(None)) - withEvent[NotAuthenticatedEvent] { - val result = controller.defaultAction(request) + withEvent[NotAuthenticatedEvent] { + val result = controller.defaultAction(request) - status(result) must equalTo(UNAUTHORIZED) - contentAsString(result) must contain("global.not.authenticated") - there was one(env.authenticatorService).discard(any(), any())(any()) - theProbe.expectMsg(500 millis, NotAuthenticatedEvent(request)) + status(result) must equalTo(UNAUTHORIZED) + contentAsString(result) must contain("global.not.authenticated") + verify(env.authenticatorService).discard(any(), any())(any()) + theProbe.expectMsg(500 millis, NotAuthenticatedEvent(request)) + } } } } "display local not-authenticated result if user isn't authenticated[authorization and error handler]" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(None)) - val result = controller.actionWithAuthorizationAndErrorHandler(request) + val result = controller.actionWithAuthorizationAndErrorHandler(request) - status(result) must equalTo(UNAUTHORIZED) - contentAsString(result) must contain("local.not.authenticated") + status(result) must equalTo(UNAUTHORIZED) + contentAsString(result) must contain("local.not.authenticated") + } } } "display local not-authenticated result if user isn't authenticated[error handler only]" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(None)) - val result = controller.actionWithErrorHandler(request) + val result = controller.actionWithErrorHandler(request) - status(result) must equalTo(UNAUTHORIZED) - contentAsString(result) must contain("local.not.authenticated") + status(result) must equalTo(UNAUTHORIZED) + contentAsString(result) must contain("local.not.authenticated") + } } } "display global not-authenticated result if user isn't authenticated" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(None)) - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(UNAUTHORIZED) - contentAsString(result) must contain("global.not.authenticated") + status(result) must equalTo(UNAUTHORIZED) + contentAsString(result) must contain("global.not.authenticated") + } } } "restrict access and update authenticator if a user is authenticated but not authorized" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - authorization.isAuthorized(any(), any())(any()) returns Future.successful(false) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + when(authorization.isAuthorized(any(), any())(any())).thenReturn(Future.successful(false)) - withEvent[NotAuthorizedEvent[FakeIdentity]] { - val result = controller.actionWithAuthorization(request) + withEvent[NotAuthorizedEvent[FakeIdentity]] { + val result = controller.actionWithAuthorization(request) - status(result) must equalTo(FORBIDDEN) - contentAsString(result) must contain("global.not.authorized") - there was one(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, NotAuthorizedEvent(identity, request)) + status(result) must equalTo(FORBIDDEN) + contentAsString(result) must contain("global.not.authorized") + verify(env.authenticatorService).update(any(), any())(any()) + theProbe.expectMsg(500 millis, NotAuthorizedEvent(identity, request)) + } } } } "display local not-authorized result if user isn't authorized" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - authorization.isAuthorized(any(), any())(any()) returns Future.successful(false) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + when(authorization.isAuthorized(any(), any())(any())).thenReturn(Future.successful(false)) - val result = controller.actionWithAuthorizationAndErrorHandler(request) + val result = controller.actionWithAuthorizationAndErrorHandler(request) - status(result) must equalTo(FORBIDDEN) - contentAsString(result) must contain("local.not.authorized") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) + status(result) must equalTo(FORBIDDEN) + contentAsString(result) must contain("local.not.authorized") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) + } } } "display global not-authorized result if user isn't authorized" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - authorization.isAuthorized(any(), any())(any()) returns Future.successful(false) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + when(authorization.isAuthorized(any(), any())(any())).thenReturn(Future.successful(false)) - val result = controller.actionWithAuthorization(request) + val result = controller.actionWithAuthorization(request) - status(result) must equalTo(FORBIDDEN) - contentAsString(result) must contain("global.not.authorized") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) + status(result) must equalTo(FORBIDDEN) + contentAsString(result) must contain("global.not.authorized") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) + } } } "invoke action without authorization if user is authenticated" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.defaultAction(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.defaultAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "invoke action with authorization if user is authenticated but not authorized" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.actionWithAuthorization(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.actionWithAuthorization(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "use next request provider in the chain if first isn't responsible" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(None) - basicAuthRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.init(any())(any[RequestHeader]()) answers { p: Any => - Future.successful(p.asInstanceOf[FakeAuthenticator#Value]) - } - env.authenticatorService.embed(any(), any[Result]())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(None)) + when(basicAuthRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.init(any())(any[RequestHeader]())).thenAnswer { p => + Future.successful(p.getArgument(0).asInstanceOf[FakeAuthenticator#Value]) + } + when(env.authenticatorService.embed(any(), any[Result]())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.actionWithAuthorization(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.actionWithAuthorization(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).init(any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).init(any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "update an initialized authenticator if it was touched" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.actionWithAuthorization(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.actionWithAuthorization(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "do not update an initialized authenticator if it was not touched" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Right(authenticator) - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.actionWithAuthorization(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).touch(any()) - there was no(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Right(authenticator)) + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.actionWithAuthorization(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "init an uninitialized authenticator" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.init(any())(any[RequestHeader]()) answers { p: Any => - Future.successful(p.asInstanceOf[FakeAuthenticator#Value]) - } - env.authenticatorService.embed(any(), any[Result]())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.init(any())(any[RequestHeader]())).thenAnswer { p => + Future.successful(p.getArgument(0).asInstanceOf[FakeAuthenticator#Value]) + } + when(env.authenticatorService.embed(any(), any[Result]())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.actionWithAuthorization(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.actionWithAuthorization(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).init(any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).init(any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "renew an initialized authenticator" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.renew(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.renew(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.renewAction(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.renewAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("renewed") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).renew(any(), any())(any()) - there was no(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("renewed") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).renew(any(), any())(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "renew an uninitialized authenticator" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.renew(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.renew(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.renewAction(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.renewAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("renewed") - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).renew(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("renewed") + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).renew(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "discard an initialized authenticator" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.discardAction(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.discardAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("discarded") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).discard(any(), any())(any()) - there was no(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + contentAsString(result) must contain("discarded") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).discard(any(), any())(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "discard an uninitialized authenticator" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.discardAction(request) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.discardAction(request) - status(result) must equalTo(OK) - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).discard(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + status(result) must equalTo(OK) + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).discard(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, request)) + } } } } "handle an Ajax request" in new InjectorContext { new WithApplication(app) with Context { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders("Accept" -> "application/json") + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders("Accept" -> "application/json") - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - withEvent[AuthenticatedEvent[FakeIdentity]] { - val result = controller.defaultAction(req) + withEvent[AuthenticatedEvent[FakeIdentity]] { + val result = controller.defaultAction(req) - status(result) must equalTo(OK) - contentType(result) must beSome("application/json") - contentAsString(result) must /("result" -> "full.access") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) - theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, req)) + status(result) must equalTo(OK) + contentType(result) must beSome("application/json") + contentAsString(result) must /("result" -> "full.access") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) + theProbe.expectMsg(500 millis, AuthenticatedEvent(identity, req)) + } } } } @@ -449,31 +493,35 @@ class SecuredActionSpec extends PlaySpecification with Mockito with JsonMatchers "The `SecuredRequestHandler`" should { "return status 401 if authentication was not successful" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) - val result = controller.defaultHandler(request) + val result = controller.defaultHandler(request) - status(result) must equalTo(UNAUTHORIZED) - there was no(env.authenticatorService).touch(any()) - there was no(env.authenticatorService).update(any(), any())(any()) + status(result) must equalTo(UNAUTHORIZED) + verify(env.authenticatorService, never()).touch(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) + } } } "return the user if authentication was successful" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - val result = controller.defaultHandler(request) + val result = controller.defaultHandler(request) - status(result) must equalTo(OK) - contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "1") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) + status(result) must equalTo(OK) + contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "1") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) + } } } } @@ -481,29 +529,33 @@ class SecuredActionSpec extends PlaySpecification with Mockito with JsonMatchers "The `exceptionHandler` method of the SecuredErrorHandler" should { "translate an ForbiddenException into a 403 Forbidden result" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } - val failed = Future.failed(new NotAuthorizedException("Access denied")) - val result = controller.recover(failed) + val failed = Future.failed(new NotAuthorizedException("Access denied")) + val result = controller.recover(failed) - status(result) must equalTo(FORBIDDEN) + status(result) must equalTo(FORBIDDEN) + } } } "translate an UnauthorizedException into a 401 Unauthorized result" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } - val failed = Future.failed(new NotAuthenticatedException("Not authenticated")) - val result = controller.recover(failed) + val failed = Future.failed(new NotAuthenticatedException("Not authenticated")) + val result = controller.recover(failed) - status(result) must equalTo(UNAUTHORIZED) + status(result) must equalTo(UNAUTHORIZED) + } } } } @@ -527,7 +579,7 @@ class SecuredActionSpec extends PlaySpecification with Mockito with JsonMatchers */ lazy val authorization = { val a = mock[Authorization[SecuredEnv#I, SecuredEnv#A]] - a.isAuthorized(any(), any())(any()) returns Future.successful(true) + when(a.isAuthorized(any(), any())(any())).thenReturn(Future.successful(true)) a } @@ -535,14 +587,14 @@ class SecuredActionSpec extends PlaySpecification with Mockito with JsonMatchers * The guice application builder. */ lazy val app = new GuiceApplicationBuilder() - .bindings(new GuiceModule) + .bindings(GuiceableModule.guiceable(new GuiceModule)) .overrides(bind[SecuredErrorHandler].to[GlobalSecuredErrorHandler]) .build() /** * The guice module. */ - class GuiceModule extends ScalaModule { + class GuiceModule extends AbstractModule with ScalaModule { override def configure(): Unit = { bind[Environment[SecuredEnv]].toInstance(env) bind[Authorization[SecuredEnv#I, SecuredEnv#A]].toInstance(authorization) @@ -755,7 +807,7 @@ object SecuredActionSpec { * * @return The result to send to the client. */ - def defaultAction = silhouette.SecuredAction { implicit request => + def defaultAction = silhouette.SecuredAction { implicit request: SecuredRequest[SecuredEnv, AnyContent] => render { case Accepts.Json() => Ok(Json.obj("result" -> "full.access")) case Accepts.Html() => Ok("full.access") diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UnsecuredActionSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UnsecuredActionSpec.scala index 36ca80a7..89078a97 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UnsecuredActionSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UnsecuredActionSpec.scala @@ -15,6 +15,8 @@ */ package io.github.honeycombcheesecake.play.silhouette.api.actions +import com.google.inject.AbstractModule + import javax.inject.Inject import akka.actor.{ Actor, ActorSystem, Props } @@ -24,15 +26,17 @@ import io.github.honeycombcheesecake.play.silhouette.api.actions.UnsecuredAction import io.github.honeycombcheesecake.play.silhouette.api.exceptions.NotAuthorizedException import io.github.honeycombcheesecake.play.silhouette.api.services.{ AuthenticatorResult, AuthenticatorService, IdentityService } import net.codingwell.scalaguice.ScalaModule -import org.specs2.control.NoLanguageFeatures import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder +import play.api.inject.guice.GuiceableModule import play.api.mvc.Results._ import play.api.mvc._ import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ +import test.Helper.mock import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -41,80 +45,90 @@ import scala.reflect.ClassTag /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.api.actions.UnsecuredActionSpec]]. */ -class UnsecuredActionSpec extends PlaySpecification with Mockito with JsonMatchers with NoLanguageFeatures { +class UnsecuredActionSpec extends PlaySpecification with JsonMatchers { "The `UnsecuredAction` action" should { "grant access if no valid authenticator can be retrieved" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(None)) - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + } } } "grant access and discard authenticator if an invalid authenticator can be retrieved" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(Some(authenticator.copy(isValid = false))) - env.authenticatorService.discard(any, any)(any) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(Some(authenticator.copy(isValid = false)))) + when(env.authenticatorService.discard(any, any)(any)).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).discard(any, any)(any) + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).discard(any, any)(any) + } } } "grant access and discard authenticator if no identity could be found for an authenticator" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(Some(authenticator)) - env.authenticatorService.discard(any, any)(any) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.discard(any, any)(any)).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(None)) - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain("full.access") - there was one(env.authenticatorService).discard(any, any)(any) + status(result) must equalTo(OK) + contentAsString(result) must contain("full.access") + verify(env.authenticatorService).discard(any, any)(any) + } } } "display local not-authorized result if user is authenticated" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any) returns Left(authenticator) - env.authenticatorService.update(any, any)(any) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any)).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any, any)(any)).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - val result = controller.actionWithErrorHandler(request) + val result = controller.actionWithErrorHandler(request) - status(result) must equalTo(FORBIDDEN) - contentAsString(result) must contain("local.not.authorized") + status(result) must equalTo(FORBIDDEN) + contentAsString(result) must contain("local.not.authorized") + } } } "display global not-authorized result if user is authenticated" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any) returns Left(authenticator) - env.authenticatorService.update(any, any)(any) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any)).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any, any)(any)).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(FORBIDDEN) - contentAsString(result) must contain("global.not.authorized") + status(result) must equalTo(FORBIDDEN) + contentAsString(result) must contain("global.not.authorized") + } } } } @@ -122,31 +136,35 @@ class UnsecuredActionSpec extends PlaySpecification with Mockito with JsonMatche "The `UnsecuredRequestHandler`" should { "return status 403 if user is authenticated" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any) returns Left(authenticator) - env.authenticatorService.update(any, any)(any) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any)).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any, any)(any)).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - val result = controller.defaultHandler(request) + val result = controller.defaultHandler(request) - status(result) must equalTo(FORBIDDEN) - there was one(env.authenticatorService).touch(any) - there was one(env.authenticatorService).update(any, any)(any) + status(result) must equalTo(FORBIDDEN) + verify(env.authenticatorService).touch(any) + verify(env.authenticatorService).update(any, any)(any) + } } } "return the data if user is not authenticated" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(None)) - val result = controller.defaultHandler(request) + val result = controller.defaultHandler(request) - status(result) must equalTo(OK) - contentAsString(result) must be equalTo "data" - there was no(env.authenticatorService).touch(any) - there was no(env.authenticatorService).update(any, any)(any) + status(result) must equalTo(OK) + contentAsString(result) must be equalTo "data" + verify(env.authenticatorService, never()).touch(any) + verify(env.authenticatorService, never()).update(any, any)(any) + } } } } @@ -154,15 +172,17 @@ class UnsecuredActionSpec extends PlaySpecification with Mockito with JsonMatche "The `exceptionHandler` method of the UnsecuredErrorHandler" should { "translate an ForbiddenException into a 403 Forbidden result" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any) returns Future.successful(None) - env.authenticatorService.discard(any, any)(any) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } + override def running() = { + when(env.authenticatorService.retrieve(any)).thenReturn(Future.successful(None)) + when(env.authenticatorService.discard(any, any)(any)).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } - val failed = Future.failed(new NotAuthorizedException("Access denied")) - val result = controller.recover(failed) + val failed = Future.failed(new NotAuthorizedException("Access denied")) + val result = controller.recover(failed) - status(result) must equalTo(FORBIDDEN) + status(result) must equalTo(FORBIDDEN) + } } } } @@ -185,14 +205,14 @@ class UnsecuredActionSpec extends PlaySpecification with Mockito with JsonMatche * The guice application builder. */ lazy val app = new GuiceApplicationBuilder() - .bindings(new GuiceModule) + .bindings(GuiceableModule.guiceable(new GuiceModule)) .overrides(bind[UnsecuredErrorHandler].to[GlobalUnsecuredErrorHandler]) .build() /** * The guice module. */ - class GuiceModule extends ScalaModule { + class GuiceModule extends AbstractModule with ScalaModule { override def configure(): Unit = { bind[Environment[UnsecuredEnv]].toInstance(env) bind[Silhouette[UnsecuredEnv]].to[SilhouetteProvider[UnsecuredEnv]] @@ -378,7 +398,7 @@ object UnsecuredActionSpec { * An unsecured request handler. */ def defaultHandler = Action.async { implicit request => - silhouette.UnsecuredRequestHandler { _ => + silhouette.UnsecuredRequestHandler { (_: Request[AnyContent]) => Future.successful(HandlerResult(Ok, Some("data"))) }.map { case HandlerResult(r, Some(data)) => Ok(data) diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareActionSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareActionSpec.scala index 0f20705c..7740aa99 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareActionSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/actions/UserAwareActionSpec.scala @@ -15,21 +15,25 @@ */ package io.github.honeycombcheesecake.play.silhouette.api.actions +import com.google.inject.AbstractModule + import javax.inject.Inject import io.github.honeycombcheesecake.play.silhouette.api._ import io.github.honeycombcheesecake.play.silhouette.api.actions.UserAwareActionSpec._ import io.github.honeycombcheesecake.play.silhouette.api.services.{ AuthenticatorResult, AuthenticatorService, IdentityService } import net.codingwell.scalaguice.ScalaModule -import org.specs2.control.NoLanguageFeatures import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.i18n.{ Lang, Langs, MessagesApi } import play.api.inject.guice.GuiceApplicationBuilder +import play.api.inject.guice.GuiceableModule import play.api.libs.json.Json import play.api.mvc.{ ControllerComponents, _ } import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ +import test.Helper.mock import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -37,217 +41,241 @@ import scala.concurrent.Future /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.api.actions.UserAwareAction]]. */ -class UserAwareActionSpec extends PlaySpecification with Mockito with JsonMatchers with NoLanguageFeatures { +class UserAwareActionSpec extends PlaySpecification with JsonMatchers { "The `UserAwareAction` action" should { "invoke action without identity and authenticator if no authenticator could be found" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain(messagesApi("without.identity.and.authenticator")) + status(result) must equalTo(OK) + contentAsString(result) must contain(messagesApi("without.identity.and.authenticator")) + } } } "invoke action without identity and authenticator if invalid authenticator was found" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator.copy(isValid = false))) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) - } + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator.copy(isValid = false)))) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain(messagesApi("without.identity.and.authenticator")) - there was one(env.authenticatorService).discard(any(), any())(any()) + status(result) must equalTo(OK) + contentAsString(result) must contain(messagesApi("without.identity.and.authenticator")) + verify(env.authenticatorService).discard(any(), any())(any()) + } } } "invoke action with valid authenticator if no identity could be found" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(None)) + + val result = controller.defaultAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain(messagesApi("without.identity.and.with.authenticator")) + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(None) - - val result = controller.defaultAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain(messagesApi("without.identity.and.with.authenticator")) - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) } } "invoke action with authenticator and identity" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.defaultAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain(messagesApi("with.identity.and.authenticator")) + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.defaultAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain(messagesApi("with.identity.and.authenticator")) - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) } } "use next request provider in the chain if first isn't responsible" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(None) - basicAuthRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.init(any())(any()) answers { p: Any => Future.successful(p.asInstanceOf[FakeAuthenticator#Value]) } - env.authenticatorService.embed(any(), any[Result]())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(None)) + when(basicAuthRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.init(any())(any())).thenAnswer { p => Future.successful(p.asInstanceOf[FakeAuthenticator#Value]) } + when(env.authenticatorService.embed(any(), any[Result]())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.defaultAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain("with.identity.and.authenticator") + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).init(any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.defaultAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain("with.identity.and.authenticator") - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).init(any())(any()) } } "update an initialized authenticator if it was touched" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + + val result = controller.defaultAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain("with.identity.and.authenticator") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) } - - val result = controller.defaultAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain("with.identity.and.authenticator") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) } } "do not update an initialized authenticator if it was not touched" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Right(authenticator) - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Right(authenticator)) + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) - val result = controller.defaultAction(request) + val result = controller.defaultAction(request) - status(result) must equalTo(OK) - contentAsString(result) must contain(messagesApi("with.identity.and.authenticator")) - there was one(env.authenticatorService).touch(any()) - there was no(env.authenticatorService).update(any(), any())(any()) + status(result) must equalTo(OK) + contentAsString(result) must contain(messagesApi("with.identity.and.authenticator")) + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) + } } } "init an uninitialized authenticator" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.init(any())(any()) answers { p: Any => Future.successful(p.asInstanceOf[FakeAuthenticator#Value]) } - env.authenticatorService.embed(any(), any[Result]())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.init(any())(any())).thenAnswer { p => Future.successful(p.asInstanceOf[FakeAuthenticator#Value]) } + when(env.authenticatorService.embed(any(), any[Result]())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.defaultAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain("with.identity.and.authenticator") + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).init(any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.defaultAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain("with.identity.and.authenticator") - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).init(any())(any()) } } "renew an initialized authenticator" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.renew(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.renew(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.renewAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain(messagesApi("renewed")) + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).renew(any(), any())(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.renewAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain(messagesApi("renewed")) - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).renew(any(), any())(any()) - there was no(env.authenticatorService).update(any(), any())(any()) } } "renew an uninitialized authenticator" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.renew(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.renew(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.renewAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain("renewed") + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).renew(any(), any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.renewAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain("renewed") - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).renew(any(), any())(any()) } } "discard an initialized authenticator" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.discardAction(request) + + status(result) must equalTo(OK) + contentAsString(result) must contain(messagesApi("discarded")) + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).discard(any(), any())(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.discardAction(request) - - status(result) must equalTo(OK) - contentAsString(result) must contain(messagesApi("discarded")) - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).discard(any(), any())(any()) - there was no(env.authenticatorService).update(any(), any())(any()) } } "discard an uninitialized authenticator" in new InjectorContext with WithRequestProvider { new WithApplication(app) with Context { - tokenRequestProvider.authenticate(any()) returns Future.successful(Some(identity.loginInfo)) - env.authenticatorService.retrieve(any()) returns Future.successful(None) - env.authenticatorService.create(any())(any()) returns Future.successful(authenticator) - env.authenticatorService.discard(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(tokenRequestProvider.authenticate(any())).thenReturn(Future.successful(Some(identity.loginInfo))) + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) + when(env.authenticatorService.create(any())(any())).thenReturn(Future.successful(authenticator)) + when(env.authenticatorService.discard(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.discardAction(request) + + status(result) must equalTo(OK) + verify(env.authenticatorService).create(any())(any()) + verify(env.authenticatorService).discard(any(), any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.discardAction(request) - - status(result) must equalTo(OK) - there was one(env.authenticatorService).create(any())(any()) - there was one(env.authenticatorService).discard(any(), any())(any()) } } } @@ -255,31 +283,35 @@ class UserAwareActionSpec extends PlaySpecification with Mockito with JsonMatche "The `UserAwareRequestHandler`" should { "return status 401 if authentication was not successful" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(None) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(None)) - val result = controller.defaultHandler(request) + val result = controller.defaultHandler(request) - status(result) must equalTo(UNAUTHORIZED) - there was no(env.authenticatorService).touch(any()) - there was no(env.authenticatorService).update(any(), any())(any()) + status(result) must equalTo(UNAUTHORIZED) + verify(env.authenticatorService, never()).touch(any()) + verify(env.authenticatorService, never()).update(any(), any())(any()) + } } } "return the user if authentication was successful" in new InjectorContext { new WithApplication(app) with Context { - env.authenticatorService.retrieve(any()) returns Future.successful(Some(authenticator)) - env.authenticatorService.touch(any()) returns Left(authenticator) - env.authenticatorService.update(any(), any())(any()) answers { (a, m) => - Future.successful(AuthenticatorResult(a.asInstanceOf[Array[Any]](1).asInstanceOf[Result])) + override def running() = { + when(env.authenticatorService.retrieve(any())).thenReturn(Future.successful(Some(authenticator))) + when(env.authenticatorService.touch(any())).thenReturn(Left(authenticator)) + when(env.authenticatorService.update(any(), any())(any())).thenAnswer { m => + Future.successful(AuthenticatorResult(m.getArgument(1).asInstanceOf[Result])) + } + when(env.identityService.retrieve(identity.loginInfo)).thenReturn(Future.successful(Some(identity))) + + val result = controller.defaultHandler(request) + + status(result) must equalTo(OK) + contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "1") + verify(env.authenticatorService).touch(any()) + verify(env.authenticatorService).update(any(), any())(any()) } - env.identityService.retrieve(identity.loginInfo) returns Future.successful(Some(identity)) - - val result = controller.defaultHandler(request) - - status(result) must equalTo(OK) - contentAsString(result) must */("providerID" -> "test") and */("providerKey" -> "1") - there was one(env.authenticatorService).touch(any()) - there was one(env.authenticatorService).update(any(), any())(any()) } } } @@ -302,13 +334,13 @@ class UserAwareActionSpec extends PlaySpecification with Mockito with JsonMatche * The guice application builder. */ lazy val app = new GuiceApplicationBuilder() - .bindings(new GuiceModule) + .bindings(GuiceableModule.guiceable(new GuiceModule)) .build() /** * The guice module. */ - class GuiceModule extends ScalaModule { + class GuiceModule extends AbstractModule with ScalaModule { override def configure(): Unit = { bind[Silhouette[UserAwareEnv]].to[SilhouetteProvider[UserAwareEnv]] bind[Environment[UserAwareEnv]].toInstance(env) @@ -433,7 +465,7 @@ object UserAwareActionSpec { * * @return The result to send to the client. */ - def defaultAction = silhouette.UserAwareAction { implicit request => + def defaultAction = silhouette.UserAwareAction { implicit request: UserAwareRequest[UserAwareEnv, AnyContent] => if (request.identity.isDefined && request.authenticator.isDefined) { Ok("with.identity.and.authenticator") } else if (request.authenticator.isDefined) { diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorResultSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorResultSpec.scala index 3bed0824..b437739b 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorResultSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/services/AuthenticatorResultSpec.scala @@ -34,28 +34,34 @@ class AuthenticatorResultSpec extends PlaySpecification { "The `withSession` method" should { "return new a new instance of an authenticator result" in new WithApplication { - val result = Results.Ok - val authenticatorResult = AuthenticatorResult(result) + override def running() = { + val result = Results.Ok + val authenticatorResult = AuthenticatorResult(result) - authenticatorResult.withSession("name" -> "value") must beAnInstanceOf[AuthenticatorResult] + authenticatorResult.withSession("name" -> "value") must beAnInstanceOf[AuthenticatorResult] + } } } "The `withCookies` method" should { "return new a new instance of an authenticator result" in new WithApplication { - val result = Results.Ok - val authenticatorResult = AuthenticatorResult(result) + override def running() = { + val result = Results.Ok + val authenticatorResult = AuthenticatorResult(result) - authenticatorResult.withCookies(Cookie("name", "value")) must beAnInstanceOf[AuthenticatorResult] + authenticatorResult.withCookies(Cookie("name", "value")) must beAnInstanceOf[AuthenticatorResult] + } } } "The `withHeaders` method" should { "return new a new instance of an authenticator result" in new WithApplication { - val result = Results.Ok - val authenticatorResult = AuthenticatorResult(result) + override def running() = { + val result = Results.Ok + val authenticatorResult = AuthenticatorResult(result) - authenticatorResult.withHeaders("name" -> "value") must beAnInstanceOf[AuthenticatorResult] + authenticatorResult.withHeaders("name" -> "value") must beAnInstanceOf[AuthenticatorResult] + } } } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/util/PlayHTTPLayerSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/util/PlayHTTPLayerSpec.scala index bbd54a01..de9cdb76 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/util/PlayHTTPLayerSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/api/util/PlayHTTPLayerSpec.scala @@ -27,13 +27,15 @@ class PlayHTTPLayerSpec extends PlaySpecification { "The `url` method" should { "return a new WS.WSRequest instance" in new WithApplication { - val url = "http://silhouette.mohiva.com" - val client = app.injector.instanceOf[WSClient] - val httpLayer = new PlayHTTPLayer(client) - val requestHolder = httpLayer.url(url) + override def running() = { + val url = "http://silhouette.mohiva.com" + val client = app.injector.instanceOf[WSClient] + val httpLayer = new PlayHTTPLayer(client) + val requestHolder = httpLayer.url(url) - requestHolder should beAnInstanceOf[WSRequest] - requestHolder.url must be equalTo url + requestHolder should beAnInstanceOf[WSRequest] + requestHolder.url must be equalTo url + } } } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/BearerTokenAuthenticatorSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/BearerTokenAuthenticatorSpec.scala index 05284333..98caa376 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/BearerTokenAuthenticatorSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/BearerTokenAuthenticatorSpec.scala @@ -22,11 +22,12 @@ import io.github.honeycombcheesecake.play.silhouette.api.repositories.Authentica import io.github.honeycombcheesecake.play.silhouette.api.services.AuthenticatorService._ import io.github.honeycombcheesecake.play.silhouette.api.util.{ RequestPart, Clock, IDGenerator } import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.BearerTokenAuthenticatorService._ -import org.specs2.control.NoLanguageFeatures -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.mvc.{ Results, AnyContentAsEmpty } import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mockSmart import java.time.ZonedDateTime import scala.concurrent.ExecutionContext.Implicits.global @@ -37,7 +38,7 @@ import scala.language.postfixOps /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.authenticators.BearerTokenAuthenticator]]. */ -class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with NoLanguageFeatures { +class BearerTokenAuthenticatorSpec extends PlaySpecification { "The `isValid` method of the authenticator" should { "return false if the authenticator is expired" in new Context { @@ -61,8 +62,8 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val id = "test-id" - idGenerator.generate returns Future.successful(id) - clock.now returns ZonedDateTime.now + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(ZonedDateTime.now) await(service.create(loginInfo)).id must be equalTo id } @@ -71,8 +72,8 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service.create(loginInfo)).lastUsedDateTime must be equalTo now } @@ -81,8 +82,8 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service.create(loginInfo)).expirationDateTime must be equalTo now + 12.hours } @@ -92,9 +93,9 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N val sixHours = 6 hours val now = ZonedDateTime.now - settings.authenticatorExpiry returns sixHours - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(settings.authenticatorExpiry).thenReturn(sixHours) + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service.create(loginInfo)).expirationDateTime must be equalTo now + sixHours } @@ -102,7 +103,7 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "throws an AuthenticatorCreationException exception if an error occurred during creation" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - idGenerator.generate returns Future.failed(new Exception("Could not generate ID")) + when(idGenerator.generate).thenReturn(Future.failed(new Exception("Could not generate ID"))) await(service.create(loginInfo)) must throwA[AuthenticatorCreationException].like { case e => @@ -121,7 +122,7 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "return None if no authenticator is stored for the token located in the headers" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> authenticator.id) - repository.find(authenticator.id) returns Future.successful(None) + when(repository.find(authenticator.id)).thenReturn(Future.successful(None)) await(service.retrieve) must beNone } @@ -129,7 +130,7 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "return authenticator if an authenticator is stored for token located in the header" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> authenticator.id) - repository.find(authenticator.id) returns Future.successful(Some(authenticator)) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator))) await(service.retrieve) must beSome(authenticator) } @@ -137,8 +138,8 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "return authenticator if an authenticator is stored for the token located in the query string" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest("GET", s"?${settings.fieldName}=${authenticator.id}") - settings.requestParts returns Some(Seq(RequestPart.QueryString)) - repository.find(authenticator.id) returns Future.successful(Some(authenticator)) + when(settings.requestParts).thenReturn(Some(Seq(RequestPart.QueryString))) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator))) await(service.retrieve) must beSome(authenticator) } @@ -146,7 +147,7 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "throws an AuthenticatorRetrievalException exception if an error occurred during retrieval" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> authenticator.id) - repository.find(authenticator.id) returns Future.failed(new RuntimeException("Cannot find authenticator")) + when(repository.find(authenticator.id)).thenReturn(Future.failed(new RuntimeException("Cannot find authenticator"))) await(service.retrieve) must throwA[AuthenticatorRetrievalException].like { case e => @@ -157,17 +158,17 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "The `init` method of the service" should { "save the authenticator in backing store" in new Context { - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[BearerTokenAuthenticator]) } + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[BearerTokenAuthenticator]) } implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val token = await(service.init(authenticator)) token must be equalTo authenticator.id - there was one(repository).add(authenticator) + verify(repository).add(authenticator) } "throws an AuthenticatorInitializationException exception if an error occurred during initialization" in new Context { - repository.add(any()) returns Future.failed(new Exception("Cannot store authenticator")) + when(repository.add(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() @@ -222,39 +223,43 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "The `touch` method of the service" should { "update the last used date if idle timeout is defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns Some(1 second) - clock.now returns ZonedDateTime.now - - service.touch(authenticator) must beLeft[BearerTokenAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo clock.now + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(Some(1 second)) + when(clock.now).thenReturn(ZonedDateTime.now) + + service.touch(authenticator) must beLeft[BearerTokenAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo clock.now + } } } "do not update the last used date if idle timeout is not defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns None - clock.now returns ZonedDateTime.now - - service.touch(authenticator) must beRight[BearerTokenAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(None) + when(clock.now).thenReturn(ZonedDateTime.now) + + service.touch(authenticator) must beRight[BearerTokenAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + } } } } "The `update` method of the service" should { "update the authenticator in backing store" in new Context { - repository.update(any()) answers { _: Any => Future.successful(authenticator) } + when(repository.update(any())).thenAnswer { _ => Future.successful(authenticator) } implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() await(service.update(authenticator, Results.Ok)) - there was one(repository).update(authenticator) + verify(repository).update(authenticator) } "return the result if the authenticator could be stored in backing store" in new Context { - repository.update(any()) answers { p: Any => Future.successful(p.asInstanceOf[BearerTokenAuthenticator]) } + when(repository.update(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[BearerTokenAuthenticator]) } implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val result = service.update(authenticator, Results.Ok) @@ -263,7 +268,7 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N } "throws an AuthenticatorUpdateException exception if an error occurred during update" in new Context { - repository.update(any()) returns Future.failed(new Exception("Cannot store authenticator")) + when(repository.update(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() @@ -280,14 +285,14 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N val now = ZonedDateTime.now val id = "new-test-id" - repository.remove(authenticator.id) returns Future.successful(()) - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[BearerTokenAuthenticator]) } - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(repository.remove(authenticator.id)).thenReturn(Future.successful(())) + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[BearerTokenAuthenticator]) } + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) await(service.renew(authenticator, Results.Ok)) - there was one(repository).remove(authenticator.id) + verify(repository).remove(authenticator.id) } "renew the authenticator and return the response with a new bearer token" in new Context { @@ -295,10 +300,10 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N val now = ZonedDateTime.now val id = "new-test-id" - repository.remove(any()) returns Future.successful(()) - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[BearerTokenAuthenticator]) } - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(repository.remove(any())).thenReturn(Future.successful(())) + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[BearerTokenAuthenticator]) } + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) val result = service.renew(authenticator, Results.Ok) @@ -310,10 +315,10 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N val now = ZonedDateTime.now val id = "new-test-id" - repository.remove(any()) returns Future.successful(()) - repository.add(any()) returns Future.failed(new Exception("Cannot store authenticator")) - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(repository.remove(any())).thenReturn(Future.successful(())) + when(repository.add(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) await(service.renew(authenticator, Results.Ok)) must throwA[AuthenticatorRenewalException].like { case e => @@ -326,18 +331,18 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N "remove authenticator from backing store" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - repository.remove(authenticator.id) returns Future.successful(authenticator) + when(repository.remove(authenticator.id)).thenReturn(Future.unit) await(service.discard(authenticator, Results.Ok)) - there was one(repository).remove(authenticator.id) + verify(repository).remove(authenticator.id) } "throws an AuthenticatorDiscardingException exception if an error occurred during discarding" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val okResult = Results.Ok - repository.remove(authenticator.id) returns Future.failed(new Exception("Cannot remove authenticator")) + when(repository.remove(authenticator.id)).thenReturn(Future.failed(new Exception("Cannot remove authenticator"))) await(service.discard(authenticator, okResult)) must throwA[AuthenticatorDiscardingException].like { case e => @@ -354,17 +359,17 @@ class BearerTokenAuthenticatorSpec extends PlaySpecification with Mockito with N /** * The repository implementation. */ - lazy val repository = mock[AuthenticatorRepository[BearerTokenAuthenticator]].smart + lazy val repository = mockSmart[AuthenticatorRepository[BearerTokenAuthenticator]] /** * The ID generator implementation. */ - lazy val idGenerator = mock[IDGenerator].smart + lazy val idGenerator = mockSmart[IDGenerator] /** * The clock implementation. */ - lazy val clock = mock[Clock].smart + lazy val clock = mockSmart[Clock] /** * The settings. diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticatorSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticatorSpec.scala index 91837f5e..35c01b39 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticatorSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/CookieAuthenticatorSpec.scala @@ -26,12 +26,13 @@ import io.github.honeycombcheesecake.play.silhouette.api.services.AuthenticatorS import io.github.honeycombcheesecake.play.silhouette.api.util.{ Clock, FingerprintGenerator, IDGenerator } import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.CookieAuthenticator._ import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.CookieAuthenticatorService._ -import org.specs2.control.NoLanguageFeatures import org.specs2.matcher.MatchResult -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.mvc.{ AnyContentAsEmpty, Cookie, DefaultCookieHeaderEncoding, Results } import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mockSmart import java.time.ZonedDateTime import scala.concurrent.ExecutionContext.Implicits.global @@ -43,7 +44,7 @@ import scala.util.{ Failure, Success } /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.authenticators.CookieAuthenticator]]. */ -class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLanguageFeatures { +class CookieAuthenticatorSpec extends PlaySpecification { "The `isValid` method of the authenticator" should { "return false if the authenticator is expired" in new Context { @@ -64,48 +65,60 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "The `serialize` method of the authenticator" should { "sign the cookie" in new WithApplication with Context { - serialize(authenticator, signer, authenticatorEncoder) + override def running() = { + serialize(authenticator, signer, authenticatorEncoder) - there was one(signer).sign(any()) + verify(signer).sign(any()) + } } "encode the cookie" in new WithApplication with Context { - serialize(authenticator, signer, authenticatorEncoder) + override def running() = { + serialize(authenticator, signer, authenticatorEncoder) - there was one(authenticatorEncoder).encode(any()) + verify(authenticatorEncoder).encode(any()) + } } } "The `unserialize` method of the authenticator" should { "throw an AuthenticatorException if the given value can't be parsed as Json" in new WithApplication with Context { - val value = "invalid" - val msg = Pattern.quote(InvalidJson.format(ID, value)) + override def running() = { + val value = "invalid" + val msg = Pattern.quote(InvalidJson.format(ID, value)) - unserialize(authenticatorEncoder.encode(value), signer, authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + unserialize(authenticatorEncoder.encode(value), signer, authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + } } "throw an AuthenticatorException if the given value is in the wrong Json format" in new WithApplication with Context { - val value = "{}" - val msg = "^" + Pattern.quote(InvalidJsonFormat.format(ID, "")) + ".*" + override def running() = { + val value = "{}" + val msg = "^" + Pattern.quote(InvalidJsonFormat.format(ID, "")) + ".*" - unserialize(authenticatorEncoder.encode(value), signer, authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + unserialize(authenticatorEncoder.encode(value), signer, authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + } } "throw an AuthenticatorException if the cookie signer declines the authenticator" in new WithApplication with Context { - val value = "value" - val msg = "^" + Pattern.quote(InvalidCookieSignature.format(ID, "")) + ".*" + override def running() = { + val value = "value" + val msg = "^" + Pattern.quote(InvalidCookieSignature.format(ID, "")) + ".*" - signer.extract(any()) returns Failure(new Exception("invalid")) + when(signer.extract(any())).thenReturn(Failure(new Exception("invalid"))) - unserialize(authenticatorEncoder.encode(value), signer, authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + unserialize(authenticatorEncoder.encode(value), signer, authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + } } } "The `serialize/unserialize` method of the authenticator" should { "serialize/unserialize an authenticator" in new WithApplication with Context { - val value = serialize(authenticator, signer, authenticatorEncoder) + override def running() = { + val value = serialize(authenticator, signer, authenticatorEncoder) - unserialize(value, signer, authenticatorEncoder) must beSuccessfulTry.withValue(authenticator) + unserialize(value, signer, authenticatorEncoder) must beSuccessfulTry.withValue(authenticator) + } } } @@ -113,10 +126,10 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "return a fingerprinted authenticator" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - idGenerator.generate returns Future.successful("test-id") - clock.now returns ZonedDateTime.now - fingerprintGenerator.generate(any()) returns "test" - settings.useFingerprinting returns true + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(ZonedDateTime.now) + when(fingerprintGenerator.generate(any())).thenReturn("test") + when(settings.useFingerprinting).thenReturn(true) await(service(Some(repository)).create(loginInfo)).fingerprint must beSome("test") } @@ -124,9 +137,9 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "return a non fingerprinted authenticator" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - idGenerator.generate returns Future.successful("test-id") - clock.now returns ZonedDateTime.now - settings.useFingerprinting returns false + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(ZonedDateTime.now) + when(settings.useFingerprinting).thenReturn(false) await(service(Some(repository)).create(loginInfo)).fingerprint must beNone } @@ -135,8 +148,8 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val id = "test-id" - idGenerator.generate returns Future.successful(id) - clock.now returns ZonedDateTime.now + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(ZonedDateTime.now) await(service(Some(repository)).create(loginInfo)).id must be equalTo id } @@ -145,8 +158,8 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service(Some(repository)).create(loginInfo)).lastUsedDateTime must be equalTo now } @@ -155,8 +168,8 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service(Some(repository)).create(loginInfo)).expirationDateTime must be equalTo now + 12.hours } @@ -166,9 +179,9 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang val sixHours = 6 hours val now = ZonedDateTime.now - settings.authenticatorExpiry returns sixHours - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(settings.authenticatorExpiry).thenReturn(sixHours) + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service(Some(repository)).create(loginInfo)).expirationDateTime must be equalTo now + sixHours } @@ -176,7 +189,7 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "throws an AuthenticatorCreationException exception if an error occurred during creation" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - idGenerator.generate returns Future.failed(new Exception("Could not generate ID")) + when(idGenerator.generate).thenReturn(Future.failed(new Exception("Could not generate ID"))) await(service(Some(repository)).create(loginInfo)) must throwA[AuthenticatorCreationException].like { case e => @@ -195,87 +208,95 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "[stateful] return None if no authenticator for the cookie is stored in backing store" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, authenticator.id)) - repository.find(authenticator.id) returns Future.successful(None) + when(repository.find(authenticator.id)).thenReturn(Future.successful(None)) await(service(Some(repository)).retrieve) must beNone } "[stateless] return None if no authenticator could be unserialized from cookie" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, authenticatorEncoder.encode("invalid"))) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, authenticatorEncoder.encode("invalid"))) - await(service(None).retrieve) must beNone - there was no(repository).find(any()) + await(service(None).retrieve) must beNone + verify(repository, never()).find(any()) + } } "[stateful] return None if authenticator fingerprint doesn't match current fingerprint" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, authenticator.id)) - fingerprintGenerator.generate(any()) returns "false" - settings.useFingerprinting returns true - authenticator.fingerprint returns Some("test") - repository.find(authenticator.id) returns Future.successful(Some(authenticator)) + when(fingerprintGenerator.generate(any())).thenReturn("false") + when(settings.useFingerprinting).thenReturn(true) + when(authenticator.fingerprint).thenReturn(Some("test")) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator))) await(service(Some(repository)).retrieve) must beNone } "[stateless] return None if authenticator fingerprint doesn't match current fingerprint" in new WithApplication with Context { - fingerprintGenerator.generate(any()) returns "false" - settings.useFingerprinting returns true - authenticator.fingerprint returns Some("test") + override def running() = { + when(fingerprintGenerator.generate(any())).thenReturn("false") + when(settings.useFingerprinting).thenReturn(true) + when(authenticator.fingerprint).thenReturn(Some("test")) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, serialize(authenticator, signer, authenticatorEncoder))) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, serialize(authenticator, signer, authenticatorEncoder))) - await(service(None).retrieve) must beNone - there was no(repository).find(any()) + await(service(None).retrieve) must beNone + verify(repository, never()).find(any()) + } } "[stateful] return authenticator if authenticator fingerprint matches current fingerprint" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, authenticator.id)) - fingerprintGenerator.generate(any()) returns "test" - settings.useFingerprinting returns true - authenticator.fingerprint returns Some("test") - repository.find(authenticator.id) returns Future.successful(Some(authenticator)) + when(fingerprintGenerator.generate(any())).thenReturn("test") + when(settings.useFingerprinting).thenReturn(true) + when(authenticator.fingerprint).thenReturn(Some("test")) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator))) await(service(Some(repository)).retrieve) must beSome(authenticator) } "[stateless] return authenticator if authenticator fingerprint matches current fingerprint" in new WithApplication with Context { - fingerprintGenerator.generate(any()) returns "test" - settings.useFingerprinting returns true - authenticator.fingerprint returns Some("test") + override def running() = { + when(fingerprintGenerator.generate(any())).thenReturn("test") + when(settings.useFingerprinting).thenReturn(true) + when(authenticator.fingerprint).thenReturn(Some("test")) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, serialize(authenticator, signer, authenticatorEncoder))) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, serialize(authenticator, signer, authenticatorEncoder))) - await(service(None).retrieve) must beSome(authenticator) - there was no(repository).find(any()) + await(service(None).retrieve) must beSome(authenticator) + verify(repository, never()).find(any()) + } } "[stateful] return authenticator if fingerprinting is disabled" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, authenticator.id)) - settings.useFingerprinting returns false - repository.find(authenticator.id) returns Future.successful(Some(authenticator)) + when(settings.useFingerprinting).thenReturn(false) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator))) await(service(Some(repository)).retrieve) must beSome(authenticator) } "[stateless] return authenticator if fingerprinting is disabled" in new WithApplication with Context { - settings.useFingerprinting returns false + override def running() = { + when(settings.useFingerprinting).thenReturn(false) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, serialize(authenticator, signer, authenticatorEncoder))) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, serialize(authenticator, signer, authenticatorEncoder))) - repository.find(authenticator.id) returns Future.successful(Some(authenticator)) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator))) - await(service(None).retrieve) must beSome(authenticator) + await(service(None).retrieve) must beSome(authenticator) + } } "throws an AuthenticatorRetrievalException exception if an error occurred during retrieval" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, authenticator.id)) - fingerprintGenerator.generate(any()) throws new RuntimeException("Could not generate fingerprint") - settings.useFingerprinting returns true - repository.find(authenticator.id) returns Future.successful(Some(authenticator)) + when(fingerprintGenerator.generate(any())).thenThrow(new RuntimeException("Could not generate fingerprint")) + when(settings.useFingerprinting).thenReturn(true) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator))) await(service(Some(repository)).retrieve) must throwA[AuthenticatorRetrievalException].like { case e => @@ -286,25 +307,27 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "The `init` method of the service" should { "[stateful] return a cookie with the authenticator ID if the authenticator could be saved in backing store" in new Context { - repository.add(any()) answers { _: Any => Future.successful(authenticator) } + when(repository.add(any())).thenAnswer { _ => Future.successful(authenticator) } implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() await(service(Some(repository)).init(authenticator)) must be equalTo statefulCookie - there was one(repository).add(any()) + verify(repository).add(any()) } "[stateless] return a cookie with a serialized authenticator" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val cookie = await(service(None).init(authenticator)) + val cookie = await(service(None).init(authenticator)) - unserialize(cookie.value, signer, authenticatorEncoder) must be equalTo unserialize(statelessCookie.value, signer, authenticatorEncoder) - there was no(repository).add(any()) + unserialize(cookie.value, signer, authenticatorEncoder) must be equalTo unserialize(statelessCookie.value, signer, authenticatorEncoder) + verify(repository, never()).add(any()) + } } "throws an AuthenticatorInitializationException exception if an error occurred during initialization" in new Context { - repository.add(any()) returns Future.failed(new Exception("Cannot store authenticator")) + when(repository.add(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() @@ -358,39 +381,43 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "The `touch` method of the service" should { "update the last used date if idle timeout is defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns Some(1 second) - clock.now returns ZonedDateTime.now - - service(Some(repository)).touch(authenticator) must beLeft[CookieAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo clock.now + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(Some(1 second)) + when(clock.now).thenReturn(ZonedDateTime.now) + + service(Some(repository)).touch(authenticator) must beLeft[CookieAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo clock.now + } } } "do not update the last used date if idle timeout is not defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns None - clock.now returns ZonedDateTime.now - - service(Some(repository)).touch(authenticator) must beRight[CookieAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(None) + when(clock.now).thenReturn(ZonedDateTime.now) + + service(Some(repository)).touch(authenticator) must beRight[CookieAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + } } } } "The `update` method of the service" should { "[stateful] update the authenticator in backing store" in new Context { - repository.update(any()) answers { _: Any => Future.successful(authenticator) } + when(repository.update(any())).thenAnswer { _ => Future.successful(authenticator) } implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() await(service(Some(repository)).update(authenticator, Results.Ok)) - there was one(repository).update(authenticator) + verify(repository).update(authenticator) } "[stateful] return the result if the authenticator could be stored in backing store" in new Context { - repository.update(any()) answers { p: Any => Future.successful(p.asInstanceOf[CookieAuthenticator]) } + when(repository.update(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[CookieAuthenticator]) } implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val result = service(Some(repository)).update(authenticator, Results.Ok) @@ -399,16 +426,18 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang } "[stateless] update the cookie for the updated authenticator" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val result = service(None).update(authenticator, Results.Ok) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val result = service(None).update(authenticator, Results.Ok) - status(result) must be equalTo OK - cookies(result).get(settings.cookieName) should beSome[Cookie].which(statelessResponseCookieMatcher(authenticator)) - there was no(repository).update(authenticator) + status(result) must be equalTo OK + cookies(result).get(settings.cookieName) should beSome[Cookie].which(statelessResponseCookieMatcher(authenticator)) + verify(repository, never()).update(authenticator) + } } "throws an AuthenticatorUpdateException exception if an error occurred during update" in new Context { - repository.update(any()) returns Future.failed(new Exception("Cannot store authenticator")) + when(repository.update(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() @@ -425,14 +454,14 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang val now = ZonedDateTime.now val id = "new-test-id" - repository.remove(authenticator.id) returns Future.successful(()) - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[CookieAuthenticator]) } - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(repository.remove(authenticator.id)).thenReturn(Future.successful(())) + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[CookieAuthenticator]) } + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) await(service(Some(repository)).renew(authenticator, Results.Ok)) - there was one(repository).remove(authenticator.id) + verify(repository).remove(authenticator.id) } "[stateful] renew the authenticator and return the response with the updated cookie value" in new Context { @@ -440,31 +469,33 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang val now = ZonedDateTime.now val id = "new-test-id" - repository.remove(any()) returns Future.successful(()) - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[CookieAuthenticator]) } - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(repository.remove(any())).thenReturn(Future.successful(())) + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[CookieAuthenticator]) } + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) val result = service(Some(repository)).renew(authenticator, Results.Ok) cookies(result).get(settings.cookieName) should beSome[Cookie].which(statefulResponseCookieMatcher(id)) - there was one(repository).add(any()) + verify(repository).add(any()) } "[stateless] renew the authenticator and return the response with the updated cookie value" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val now = ZonedDateTime.now - val id = "new-test-id" + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val now = ZonedDateTime.now + val id = "new-test-id" - settings.useFingerprinting returns false - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(settings.useFingerprinting).thenReturn(false) + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) - val result = service(None).renew(authenticator, Results.Ok) + val result = service(None).renew(authenticator, Results.Ok) - cookies(result).get(settings.cookieName) should beSome[Cookie].which(statelessResponseCookieMatcher( - authenticator.copy(id = id, lastUsedDateTime = now, expirationDateTime = now + settings.authenticatorExpiry))) - there was no(repository).add(any()) + cookies(result).get(settings.cookieName) should beSome[Cookie].which(statelessResponseCookieMatcher( + authenticator.copy(id = id, lastUsedDateTime = now, expirationDateTime = now + settings.authenticatorExpiry))) + verify(repository, never()).add(any()) + } } "throws an AuthenticatorRenewalException exception if an error occurred during renewal" in new Context { @@ -472,10 +503,10 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang val now = ZonedDateTime.now val id = "new-test-id" - repository.remove(any()) returns Future.successful(()) - repository.add(any()) returns Future.failed(new Exception("Cannot store authenticator")) - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(repository.remove(any())).thenReturn(Future.successful(())) + when(repository.add(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) await(service(Some(repository)).renew(authenticator, Results.Ok)) must throwA[AuthenticatorRenewalException].like { case e => @@ -488,7 +519,7 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang "[stateful] discard the cookie from response and remove it from backing store" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - repository.remove(any()) returns Future.successful(()) + when(repository.remove(any())).thenReturn(Future.successful(())) val result = service(Some(repository)).discard(authenticator, Results.Ok.withCookies(statefulCookie)) @@ -500,29 +531,31 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang c.domain must be equalTo settings.cookieDomain c.secure must be equalTo settings.secureCookie } - there was one(repository).remove(authenticator.id) + verify(repository).remove(authenticator.id) } "[stateless] discard the cookie from response" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val result = service(None).discard(authenticator, Results.Ok.withCookies(statelessCookie)) - - cookies(result).get(settings.cookieName) should beSome[Cookie].which { c => - c.name must be equalTo settings.cookieName - c.value must be equalTo "" - c.maxAge must beSome(Cookie.DiscardedMaxAge) - c.path must be equalTo settings.cookiePath - c.domain must be equalTo settings.cookieDomain - c.secure must be equalTo settings.secureCookie + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val result = service(None).discard(authenticator, Results.Ok.withCookies(statelessCookie)) + + cookies(result).get(settings.cookieName) should beSome[Cookie].which { c => + c.name must be equalTo settings.cookieName + c.value must be equalTo "" + c.maxAge must beSome(Cookie.DiscardedMaxAge) + c.path must be equalTo settings.cookiePath + c.domain must be equalTo settings.cookieDomain + c.secure must be equalTo settings.secureCookie + } + verify(repository, never()).remove(authenticator.id) } - there was no(repository).remove(authenticator.id) } "throws an AuthenticatorDiscardingException exception if an error occurred during discarding" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val okResult = Results.Ok - repository.remove(any()) returns Future.failed(new Exception("Cannot store authenticator")) + when(repository.remove(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) await(service(Some(repository)).discard(authenticator, okResult)) must throwA[AuthenticatorDiscardingException].like { case e => @@ -539,27 +572,27 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang /** * The repository implementation. */ - lazy val repository = mock[AuthenticatorRepository[CookieAuthenticator]].smart + lazy val repository = mockSmart[AuthenticatorRepository[CookieAuthenticator]] /** * The ID generator implementation. */ - lazy val fingerprintGenerator = mock[FingerprintGenerator].smart + lazy val fingerprintGenerator = mockSmart[FingerprintGenerator] /** * The ID generator implementation. */ - lazy val idGenerator = mock[IDGenerator].smart + lazy val idGenerator = mockSmart[IDGenerator] /** * The signer implementation. * - * The signer returns the same value as passed to the methods. This is enough for testing. + * The signer).thenReturn(the same value as passed to the methods. This is enough for testing.) */ lazy val signer = { - val c = mock[Signer].smart - c.sign(any()) answers { p: Any => p.asInstanceOf[String] } - c.extract(any()) answers { p: Any => Success(p.asInstanceOf[String]) } + val c = mockSmart[Signer] + when(c.sign(any())).thenAnswer { _.getArgument(0).asInstanceOf[String] } + when(c.extract(any())).thenAnswer { p => Success(p.getArgument(0).asInstanceOf[String]) } c } @@ -574,7 +607,7 @@ class CookieAuthenticatorSpec extends PlaySpecification with Mockito with NoLang /** * The clock implementation. */ - lazy val clock = mock[Clock].smart + lazy val clock = mockSmart[Clock] /** * The settings. diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/DummyAuthenticatorSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/DummyAuthenticatorSpec.scala index a28d7978..bfe3d49c 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/DummyAuthenticatorSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/DummyAuthenticatorSpec.scala @@ -17,7 +17,6 @@ package io.github.honeycombcheesecake.play.silhouette.impl.authenticators import io.github.honeycombcheesecake.play.silhouette.api.LoginInfo import io.github.honeycombcheesecake.play.silhouette.api.services.AuthenticatorResult -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.mvc.{ AnyContentAsEmpty, Results } import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } @@ -27,7 +26,7 @@ import scala.concurrent.ExecutionContext.Implicits.global /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.authenticators.DummyAuthenticator]]. */ -class DummyAuthenticatorSpec extends PlaySpecification with Mockito { +class DummyAuthenticatorSpec extends PlaySpecification { "The `isValid` method of the authenticator" should { "return true" in new Context { @@ -78,9 +77,11 @@ class DummyAuthenticatorSpec extends PlaySpecification with Mockito { "The `touch` method of the service" should { "not update the authenticator" in new WithApplication with Context { - service.touch(authenticator) must beRight[DummyAuthenticator].like { - case a => - a.loginInfo must be equalTo loginInfo + override def running() = { + service.touch(authenticator) must beRight[DummyAuthenticator].like { + case a => + a.loginInfo must be equalTo loginInfo + } } } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/JWTAuthenticatorSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/JWTAuthenticatorSpec.scala index 33fabf57..b0b1d68e 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/JWTAuthenticatorSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/JWTAuthenticatorSpec.scala @@ -25,13 +25,14 @@ import io.github.honeycombcheesecake.play.silhouette.api.services.AuthenticatorS import io.github.honeycombcheesecake.play.silhouette.api.util.{ Clock, IDGenerator, RequestPart } import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.JWTAuthenticator._ import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.JWTAuthenticatorService._ -import org.specs2.control.NoLanguageFeatures import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.json.{ JsNull, JsObject, Json } import play.api.mvc.{ AnyContentAsEmpty, Results } import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mockSmart import java.time.temporal.ChronoField import java.time.{ ZoneId, ZonedDateTime } @@ -43,7 +44,7 @@ import scala.language.postfixOps /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.authenticators.JWTAuthenticator]]. */ -class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatchers with NoLanguageFeatures { +class JWTAuthenticatorSpec extends PlaySpecification with JsonMatchers { "The `isValid` method of the authenticator" should { "return false if the authenticator is expired" in new Context { @@ -64,128 +65,153 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch "The `serialize` method of the authenticator" should { "return a JWT with an expiration time" in new WithApplication with Context { - val jwt = serialize(authenticator, authenticatorEncoder, settings) - val json = Base64.decode(jwt.split('.').apply(1)) + override def running() = { + val jwt = serialize(authenticator, authenticatorEncoder, settings) + val json = Base64.decode(jwt.split('.').apply(1)) - json must /("exp" -> authenticator.expirationDateTime.toEpochSecond.toInt) + json must /("exp" -> authenticator.expirationDateTime.toEpochSecond.toInt) + } } "return a JWT with an encoded subject" in new WithApplication with Context { - val jwt = serialize(authenticator, authenticatorEncoder, settings) - val json = Json.parse(Base64.decode(jwt.split('.').apply(1))) - val sub = Json.parse(authenticatorEncoder.decode((json \ "sub").as[String])).as[LoginInfo] + override def running() = { + val jwt = serialize(authenticator, authenticatorEncoder, settings) + val json = Json.parse(Base64.decode(jwt.split('.').apply(1))) + val sub = Json.parse(authenticatorEncoder.decode((json \ "sub").as[String])).as[LoginInfo] - sub must be equalTo authenticator.loginInfo + sub must be equalTo authenticator.loginInfo + } } "return a JWT with an issuer" in new WithApplication with Context { - val jwt = serialize(authenticator, authenticatorEncoder, settings) - val json = Base64.decode(jwt.split('.').apply(1)) + override def running() = { + val jwt = serialize(authenticator, authenticatorEncoder, settings) + val json = Base64.decode(jwt.split('.').apply(1)) - json must /("iss" -> settings.issuerClaim) + json must /("iss" -> settings.issuerClaim) + } } "return a JWT with an issued-at time" in new WithApplication with Context { - val jwt = serialize(authenticator, authenticatorEncoder, settings) - val json = Base64.decode(jwt.split('.').apply(1)) + override def running() = { + val jwt = serialize(authenticator, authenticatorEncoder, settings) + val json = Base64.decode(jwt.split('.').apply(1)) - json must /("iat" -> authenticator.lastUsedDateTime.toEpochSecond.toInt) + json must /("iat" -> authenticator.lastUsedDateTime.toEpochSecond.toInt) + } } "throw an AuthenticatorException if a reserved claim will be overridden" in new WithApplication with Context { - val claims = Json.obj( - "jti" -> "reserved") + override def running() = { + val claims = Json.obj( + "jti" -> "reserved") - serialize(authenticator.copy(customClaims = Some(claims)), authenticatorEncoder, settings) must throwA[AuthenticatorException].like { - case e => e.getMessage must startWith(OverrideReservedClaim.format(ID, "jti", "")) + serialize(authenticator.copy(customClaims = Some(claims)), authenticatorEncoder, settings) must throwA[AuthenticatorException].like { + case e => e.getMessage must startWith(OverrideReservedClaim.format(ID, "jti", "")) + } } } "throw an AuthenticatorException if an unexpected value was found in the arbitrary claims" in new WithApplication with Context { - val claims = Json.obj( - "null" -> JsNull) + override def running() = { + val claims = Json.obj( + "null" -> JsNull) - serialize(authenticator.copy(customClaims = Some(claims)), authenticatorEncoder, settings) must throwA[AuthenticatorException].like { - case e => e.getMessage must startWith(UnexpectedJsonValue.format(ID, "")) + serialize(authenticator.copy(customClaims = Some(claims)), authenticatorEncoder, settings) must throwA[AuthenticatorException].like { + case e => e.getMessage must startWith(UnexpectedJsonValue.format(ID, "")) + } } } "return a JWT with arbitrary claims" in new WithApplication with Context { - val jwt = serialize(authenticator.copy(customClaims = Some(customClaims)), authenticatorEncoder, settings) - val json = Base64.decode(jwt.split('.').apply(1)) - - json must /("boolean" -> true) - json must /("string" -> "string") - json must /("number" -> 1234567890) - json must /("array") /# 0 / 1 - json must /("array") /# 1 / 2 - json must /("object") / "array" /# 0 / "string1" - json must /("object") / "array" /# 1 / "string2" - json must /("object") / "object" / "array" /# 0 / "string" - json must /("object") / "object" / "array" /# 1 / false - json must /("object") / "object" / "array" /# 2 / ("number" -> 1) + override def running() = { + val jwt = serialize(authenticator.copy(customClaims = Some(customClaims)), authenticatorEncoder, settings) + val json = Base64.decode(jwt.split('.').apply(1)) + + json must /("boolean" -> true) + json must /("string" -> "string") + json must /("number" -> 1234567890) + json must /("array") /# 0 / 1 + json must /("array") /# 1 / 2 + json must /("object") / "array" /# 0 / "string1" + json must /("object") / "array" /# 1 / "string2" + json must /("object") / "object" / "array" /# 0 / "string" + json must /("object") / "object" / "array" /# 1 / false + json must /("object") / "object" / "array" /# 2 / ("number" -> 1) + } } } "The `unserialize` method of the authenticator" should { "throw an AuthenticatorException if the given token can't be parsed" in new WithApplication with Context { - val jwt = "invalid" - val msg = Pattern.quote(InvalidJWTToken.format(ID, jwt)) + override def running() = { + val jwt = "invalid" + val msg = Pattern.quote(InvalidJWTToken.format(ID, jwt)) - unserialize(jwt, authenticatorEncoder, settings) must beFailedTry.withThrowable[AuthenticatorException](msg) + unserialize(jwt, authenticatorEncoder, settings) must beFailedTry.withThrowable[AuthenticatorException](msg) + } } "throw an AuthenticatorException if the given token couldn't be verified" in new WithApplication with Context { - val jwt = serialize(authenticator, authenticatorEncoder, settings) + "-wrong-sig" - val msg = Pattern.quote(InvalidJWTToken.format(ID, jwt)) + override def running() = { + val jwt = serialize(authenticator, authenticatorEncoder, settings) + "-wrong-sig" + val msg = Pattern.quote(InvalidJWTToken.format(ID, jwt)) - unserialize(jwt, authenticatorEncoder, settings) must beFailedTry.withThrowable[AuthenticatorException](msg) + unserialize(jwt, authenticatorEncoder, settings) must beFailedTry.withThrowable[AuthenticatorException](msg) + } } "unserialize a JWT" in new WithApplication with Context { - val jwt = serialize(authenticator, authenticatorEncoder, settings) + override def running() = { + val jwt = serialize(authenticator, authenticatorEncoder, settings) - unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.withValue(authenticator.copy( - expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.withValue(authenticator.copy( + expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + } } "unserialize a JWT with a custom clock" in new WithApplication with Context { + override def running() = { + val lastUsedDateTime: ZonedDateTime = ZonedDateTime + .of(2015, 2, 25, 19, 0, 0, 0, ZoneId.systemDefault()) + .`with`(ChronoField.MILLI_OF_SECOND, 0) - val lastUsedDateTime: ZonedDateTime = ZonedDateTime - .of(2015, 2, 25, 19, 0, 0, 0, ZoneId.systemDefault()) - .`with`(ChronoField.MILLI_OF_SECOND, 0) - - val authenticatorCustomClock: JWTAuthenticator = authenticator - .copy( - expirationDateTime = lastUsedDateTime + settings.authenticatorExpiry, - lastUsedDateTime = lastUsedDateTime) + val authenticatorCustomClock: JWTAuthenticator = authenticator + .copy( + expirationDateTime = lastUsedDateTime + settings.authenticatorExpiry, + lastUsedDateTime = lastUsedDateTime) - val jwt: String = serialize(authenticatorCustomClock, authenticatorEncoder, settings) + val jwt: String = serialize(authenticatorCustomClock, authenticatorEncoder, settings) - clock.now returns lastUsedDateTime - implicit val customClock: Option[Clock] = Some(clock) + when(clock.now).thenReturn(lastUsedDateTime) + implicit val customClock: Option[Clock] = Some(clock) - unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.withValue(authenticatorCustomClock.copy( - expirationDateTime = authenticatorCustomClock.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticatorCustomClock.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.withValue(authenticatorCustomClock.copy( + expirationDateTime = authenticatorCustomClock.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticatorCustomClock.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + } } "unserialize a JWT with arbitrary claims" in new WithApplication with Context { - val jwt = serialize(authenticator.copy(customClaims = Some(customClaims)), authenticatorEncoder, settings) + override def running() = { + val jwt = serialize(authenticator.copy(customClaims = Some(customClaims)), authenticatorEncoder, settings) - unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.like { - case a => - a.customClaims must beSome(customClaims) + unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.like { + case a => + a.customClaims must beSome(customClaims) + } } } } "The `serialize/unserialize` method of the authenticator" should { "serialize/unserialize an authenticator" in new WithApplication with Context { - val jwt = serialize(authenticator, authenticatorEncoder, settings) + override def running() = { + val jwt = serialize(authenticator, authenticatorEncoder, settings) - unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.withValue(authenticator) + unserialize(jwt, authenticatorEncoder, settings) must beSuccessfulTry.withValue(authenticator) + } } } @@ -194,8 +220,8 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val id = "test-id" - idGenerator.generate returns Future.successful(id) - clock.now returns ZonedDateTime.now + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(ZonedDateTime.now) await(service(None).create(loginInfo)).id must be equalTo id } @@ -204,8 +230,8 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service(None).create(loginInfo)).lastUsedDateTime must be equalTo now } @@ -214,8 +240,8 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service(None).create(loginInfo)).expirationDateTime must be equalTo now + 12.hours } @@ -225,9 +251,9 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch val sixHours = 6 hours val now = ZonedDateTime.now - settings.authenticatorExpiry returns sixHours - idGenerator.generate returns Future.successful("test-id") - clock.now returns now + when(settings.authenticatorExpiry).thenReturn(sixHours) + when(idGenerator.generate).thenReturn(Future.successful("test-id")) + when(clock.now).thenReturn(now) await(service(None).create(loginInfo)).expirationDateTime must be equalTo now + sixHours } @@ -235,7 +261,7 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch "throws an AuthenticatorCreationException exception if an error occurred during creation" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - idGenerator.generate returns Future.failed(new Exception("Could not generate ID")) + when(idGenerator.generate).thenReturn(Future.failed(new Exception("Could not generate ID"))) await(service(None).create(loginInfo)) must throwA[AuthenticatorCreationException].like { case e => @@ -254,94 +280,108 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch "return None if DAO is enabled and no authenticator is stored for the token located in the header" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> "not-stored") - repository.find(authenticator.id) returns Future.successful(None) + when(repository.find(authenticator.id)).thenReturn(Future.successful(None)) await(service(Some(repository)).retrieve) must beNone } "return authenticator if DAO is enabled and an authenticator is stored for the token located in the the header" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> serialize(authenticator, authenticatorEncoder, settings)) - clock.now returns ZonedDateTime.now + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> serialize(authenticator, authenticatorEncoder, settings)) + when(clock.now).thenReturn(ZonedDateTime.now) - repository.find(authenticator.id) returns Future.successful(Some(authenticator.copy( - expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0)))) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator.copy( + expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))))) - await(service(Some(repository)).retrieve) must beSome(authenticator.copy( - expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + await(service(Some(repository)).retrieve) must beSome(authenticator.copy( + expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + } } "return authenticator if DAO is enabled and an authenticator is stored for the token located in the the query string" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest("GET", s"?${settings.fieldName}=${serialize(authenticator, authenticatorEncoder, settings)}") - clock.now returns ZonedDateTime.now - - settings.requestParts returns Some(Seq(RequestPart.QueryString)) - repository.find(authenticator.id) returns Future.successful(Some(authenticator.copy( - expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0)))) - - await(service(Some(repository)).retrieve) must beSome(authenticator.copy( - expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest("GET", s"?${settings.fieldName}=${serialize(authenticator, authenticatorEncoder, settings)}") + when(clock.now).thenReturn(ZonedDateTime.now) + + when(settings.requestParts).thenReturn(Some(Seq(RequestPart.QueryString))) + when(repository.find(authenticator.id)).thenReturn(Future.successful(Some(authenticator.copy( + expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))))) + + await(service(Some(repository)).retrieve) must beSome(authenticator.copy( + expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + } } "return authenticator if DAO is disabled and authenticator was found in the header" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> serialize(authenticator, authenticatorEncoder, settings)) - clock.now returns ZonedDateTime.now - - await(service(None).retrieve) must beSome(authenticator.copy( - expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) - there was no(repository).find(any()) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> serialize(authenticator, authenticatorEncoder, settings)) + when(clock.now).thenReturn(ZonedDateTime.now) + + await(service(None).retrieve) must beSome(authenticator.copy( + expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + verify(repository, never()).find(any()) + } } "return authenticator if DAO is disabled and authenticator was found in the query string" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest("GET", s"?${settings.fieldName}=${serialize(authenticator, authenticatorEncoder, settings)}") - clock.now returns ZonedDateTime.now - - settings.requestParts returns Some(Seq(RequestPart.QueryString)) - await(service(None).retrieve) must beSome(authenticator.copy( - expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), - lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) - there was no(repository).find(any()) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest("GET", s"?${settings.fieldName}=${serialize(authenticator, authenticatorEncoder, settings)}") + when(clock.now).thenReturn(ZonedDateTime.now) + + when(settings.requestParts).thenReturn(Some(Seq(RequestPart.QueryString))) + await(service(None).retrieve) must beSome(authenticator.copy( + expirationDateTime = authenticator.expirationDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0), + lastUsedDateTime = authenticator.lastUsedDateTime.`with`(ChronoField.MILLI_OF_SECOND, 0))) + verify(repository, never()).find(any()) + } } "throws an AuthenticatorRetrievalException exception if an error occurred during retrieval" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> serialize(authenticator, authenticatorEncoder, settings)) - clock.now returns ZonedDateTime.now + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders(settings.fieldName -> serialize(authenticator, authenticatorEncoder, settings)) + when(clock.now).thenReturn(ZonedDateTime.now) - repository.find(authenticator.id) returns Future.failed(new RuntimeException("Cannot find authenticator")) + when(repository.find(authenticator.id)).thenReturn(Future.failed(new RuntimeException("Cannot find authenticator"))) - await(service(Some(repository)).retrieve) must throwA[AuthenticatorRetrievalException].like { - case e => - e.getMessage must startWith(RetrieveError.format(ID, "")) + await(service(Some(repository)).retrieve) must throwA[AuthenticatorRetrievalException].like { + case e => + e.getMessage must startWith(RetrieveError.format(ID, "")) + } } } } "The `init` method of the service" should { "return the token if DAO is enabled and authenticator could be saved in backing store" in new WithApplication with Context { - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[JWTAuthenticator]) } - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[JWTAuthenticator]) } + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val token = await(service(Some(repository)).init(authenticator)) + val token = await(service(Some(repository)).init(authenticator)) - unserialize(token, authenticatorEncoder, settings).get must be equalTo authenticator - there was one(repository).add(any()) + unserialize(token, authenticatorEncoder, settings).get must be equalTo authenticator + verify(repository).add(any()) + } } "return the token if DAO is disabled" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val token = await(service(None).init(authenticator)) + val token = await(service(None).init(authenticator)) - unserialize(token, authenticatorEncoder, settings).get must be equalTo authenticator - there was no(repository).add(any()) + unserialize(token, authenticatorEncoder, settings).get must be equalTo authenticator + verify(repository, never()).add(any()) + } } "throws an AuthenticatorInitializationException exception if an error occurred during initialization" in new Context { - repository.add(any()) returns Future.failed(new Exception("Cannot store authenticator")) + when(repository.add(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val okResult = Future.successful(Results.Ok) @@ -355,171 +395,199 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch "The result `embed` method of the service" should { "return the response with a header" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val token = serialize(authenticator, authenticatorEncoder, settings) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val token = serialize(authenticator, authenticatorEncoder, settings) - val result = service(Some(repository)).embed(token, Results.Ok) + val result = service(Some(repository)).embed(token, Results.Ok) - header(settings.fieldName, result) should beSome(token) + header(settings.fieldName, result) should beSome(token) + } } } "The request `embed` method of the service" should { "return the request with a header " in new WithApplication with Context { - val token = serialize(authenticator, authenticatorEncoder, settings) - val request = service(Some(repository)).embed(token, FakeRequest()) + override def running() = { + val token = serialize(authenticator, authenticatorEncoder, settings) + val request = service(Some(repository)).embed(token, FakeRequest()) - unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator + unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator + } } "override an existing token" in new WithApplication with Context { - val token = serialize(authenticator, authenticatorEncoder, settings) - val request = service(Some(repository)).embed(token, FakeRequest().withHeaders(settings.fieldName -> "test")) + override def running() = { + val token = serialize(authenticator, authenticatorEncoder, settings) + val request = service(Some(repository)).embed(token, FakeRequest().withHeaders(settings.fieldName -> "test")) - unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator + unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator + } } "keep non authenticator related headers" in new WithApplication with Context { - val token = serialize(authenticator, authenticatorEncoder, settings) - val request = service(Some(repository)).embed(token, FakeRequest().withHeaders("test" -> "test")) + override def running() = { + val token = serialize(authenticator, authenticatorEncoder, settings) + val request = service(Some(repository)).embed(token, FakeRequest().withHeaders("test" -> "test")) - unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator - request.headers.get("test") should beSome("test") + unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator + request.headers.get("test") should beSome("test") + } } "keep other request parts" in new WithApplication with Context { - val token = serialize(authenticator, authenticatorEncoder, settings) - val request = service(Some(repository)).embed(token, FakeRequest().withSession("test" -> "test")) + override def running() = { + val token = serialize(authenticator, authenticatorEncoder, settings) + val request = service(Some(repository)).embed(token, FakeRequest().withSession("test" -> "test")) - unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator - request.session.get("test") should beSome("test") + unserialize(request.headers.get(settings.fieldName).get, authenticatorEncoder, settings).get must be equalTo authenticator + request.session.get("test") should beSome("test") + } } } "The `touch` method of the service" should { "update the last used date if idle timeout is defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns Some(1 second) - clock.now returns ZonedDateTime.now - - service(None).touch(authenticator) must beLeft[JWTAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo clock.now + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(Some(1 second)) + when(clock.now).thenReturn(ZonedDateTime.now) + + service(None).touch(authenticator) must beLeft[JWTAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo clock.now + } } } "do not update the last used date if idle timeout is not defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns None - clock.now returns ZonedDateTime.now - - service(None).touch(authenticator) must beRight[JWTAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(None) + when(clock.now).thenReturn(ZonedDateTime.now) + + service(None).touch(authenticator) must beRight[JWTAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + } } } } "The `update` method of the service" should { "update the authenticator in backing store" in new WithApplication with Context { - repository.update(any()) answers { _: Any => Future.successful(authenticator) } + override def running() = { + when(repository.update(any())).thenAnswer { _ => Future.successful(authenticator) } - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - await(service(Some(repository)).update(authenticator, Results.Ok)) + await(service(Some(repository)).update(authenticator, Results.Ok)) - there was one(repository).update(authenticator) + verify(repository).update(authenticator) + } } "return the result if the authenticator could be stored in backing store" in new WithApplication with Context { - repository.update(any()) answers { p: Any => Future.successful(p.asInstanceOf[JWTAuthenticator]) } + override def running() = { + when(repository.update(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[JWTAuthenticator]) } - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val result = service(Some(repository)).update(authenticator, Results.Ok) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val result = service(Some(repository)).update(authenticator, Results.Ok) - status(result) must be equalTo OK - unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator - there was one(repository).update(authenticator) + status(result) must be equalTo OK + unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator + verify(repository).update(authenticator) + } } "return the result if backing store is disabled" in new WithApplication with Context { - repository.update(any()) answers { p: Any => Future.successful(p.asInstanceOf[JWTAuthenticator]) } + override def running() = { + when(repository.update(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[JWTAuthenticator]) } - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val result = service(None).update(authenticator, Results.Ok) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val result = service(None).update(authenticator, Results.Ok) - status(result) must be equalTo OK - unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator - there was no(repository).update(any()) + status(result) must be equalTo OK + unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator + verify(repository, never()).update(any()) + } } "throws an AuthenticatorUpdateException exception if an error occurred during update" in new WithApplication with Context { - repository.update(any()) returns Future.failed(new Exception("Cannot store authenticator")) + override def running() = { + when(repository.update(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - await(service(Some(repository)).update(authenticator, Results.Ok)) must throwA[AuthenticatorUpdateException].like { - case e => - e.getMessage must startWith(UpdateError.format(ID, "")) + await(service(Some(repository)).update(authenticator, Results.Ok)) must throwA[AuthenticatorUpdateException].like { + case e => + e.getMessage must startWith(UpdateError.format(ID, "")) + } } } } "The `renew` method of the service" should { "renew the authenticator and return the response with a new JWT if DAO is enabled" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val id = "new-test-id" + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val id = "new-test-id" - repository.remove(any()) answers { _: Any => Future.successful(()) } - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[JWTAuthenticator]) } - idGenerator.generate returns Future.successful(id) - clock.now returns ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0) + when(repository.remove(any())).thenAnswer { _ => Future.successful(()) } + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[JWTAuthenticator]) } + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0)) - val result = service(Some(repository)).renew(authenticator, Results.Ok) + val result = service(Some(repository)).renew(authenticator, Results.Ok) - unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator.copy( - id = id, - expirationDateTime = clock.now + settings.authenticatorExpiry, - lastUsedDateTime = clock.now) + unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator.copy( + id = id, + expirationDateTime = clock.now + settings.authenticatorExpiry, + lastUsedDateTime = clock.now) - there was one(repository).add(any()) - there was one(repository).remove(authenticator.id) + verify(repository).add(any()) + verify(repository).remove(authenticator.id) + } } "renew an authenticator with custom claims" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val id = "new-test-id" + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val id = "new-test-id" - repository.remove(any()) returns Future.successful(()) - repository.add(any()) answers { p: Any => Future.successful(p.asInstanceOf[JWTAuthenticator]) } - idGenerator.generate returns Future.successful(id) - clock.now returns ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0) + when(repository.remove(any())).thenReturn(Future.successful(())) + when(repository.add(any())).thenAnswer { p => Future.successful(p.getArgument(0).asInstanceOf[JWTAuthenticator]) } + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0)) - val result = service(Some(repository)).renew(authenticator.copy(customClaims = Some(customClaims)), Results.Ok) + val result = service(Some(repository)).renew(authenticator.copy(customClaims = Some(customClaims)), Results.Ok) - unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator.copy( - id = id, - expirationDateTime = clock.now + settings.authenticatorExpiry, - lastUsedDateTime = clock.now, - customClaims = Some(customClaims)) + unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator.copy( + id = id, + expirationDateTime = clock.now + settings.authenticatorExpiry, + lastUsedDateTime = clock.now, + customClaims = Some(customClaims)) - there was one(repository).add(any()) - there was one(repository).remove(authenticator.id) + verify(repository).add(any()) + verify(repository).remove(authenticator.id) + } } "renew the authenticator and return the response with a new JWT if DAO is disabled" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val id = "new-test-id" + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val id = "new-test-id" - idGenerator.generate returns Future.successful(id) - clock.now returns ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0) + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0)) - val result = service(None).renew(authenticator, Results.Ok) + val result = service(None).renew(authenticator, Results.Ok) - unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator.copy( - id = id, - expirationDateTime = clock.now + settings.authenticatorExpiry, - lastUsedDateTime = clock.now) - there was no(repository).remove(any()) - there was no(repository).add(any()) + unserialize(header(settings.fieldName, result).get, authenticatorEncoder, settings).get must be equalTo authenticator.copy( + id = id, + expirationDateTime = clock.now + settings.authenticatorExpiry, + lastUsedDateTime = clock.now) + verify(repository, never()).remove(any()) + verify(repository, never()).add(any()) + } } "throws an AuthenticatorRenewalException exception if an error occurred during renewal" in new Context { @@ -527,10 +595,10 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch val now = ZonedDateTime.now val id = "new-test-id" - repository.remove(any()) returns Future.successful(()) - repository.add(any()) returns Future.failed(new Exception("Cannot store authenticator")) - idGenerator.generate returns Future.successful(id) - clock.now returns now + when(repository.remove(any())).thenReturn(Future.successful(())) + when(repository.add(any())).thenReturn(Future.failed(new Exception("Cannot store authenticator"))) + when(idGenerator.generate).thenReturn(Future.successful(id)) + when(clock.now).thenReturn(now) await(service(Some(repository)).renew(authenticator, Results.Ok)) must throwA[AuthenticatorRenewalException].like { case e => @@ -543,11 +611,11 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch "remove authenticator from backing store if DAO is enabled" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - repository.remove(authenticator.id) returns Future.successful(authenticator) + when(repository.remove(authenticator.id)).thenReturn(Future.unit) await(service(Some(repository)).discard(authenticator, Results.Ok)) - there was one(repository).remove(authenticator.id) + verify(repository).remove(authenticator.id) } "do not remove the authenticator from backing store if DAO is disabled" in new Context { @@ -555,14 +623,14 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch await(service(None).discard(authenticator, Results.Ok)) - there was no(repository).remove(authenticator.id) + verify(repository, never()).remove(authenticator.id) } "throws an AuthenticatorDiscardingException exception if an error occurred during discarding" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val okResult = Results.Ok - repository.remove(authenticator.id) returns Future.failed(new Exception("Cannot remove authenticator")) + when(repository.remove(authenticator.id)).thenReturn(Future.failed(new Exception("Cannot remove authenticator"))) await(service(Some(repository)).discard(authenticator, okResult)) must throwA[AuthenticatorDiscardingException].like { case e => @@ -576,12 +644,12 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch */ trait Context extends Scope { - private val lastUsedDateTime = ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0) + private lazy val lastUsedDateTime = ZonedDateTime.now.`with`(ChronoField.MILLI_OF_SECOND, 0) /** * The repository implementation. */ - lazy val repository = mock[AuthenticatorRepository[JWTAuthenticator]].smart + lazy val repository = mockSmart[AuthenticatorRepository[JWTAuthenticator]] /** * The authenticator encoder implementation. @@ -591,12 +659,12 @@ class JWTAuthenticatorSpec extends PlaySpecification with Mockito with JsonMatch /** * The ID generator implementation. */ - lazy val idGenerator = mock[IDGenerator].smart + lazy val idGenerator = mockSmart[IDGenerator] /** * The clock implementation. */ - lazy val clock: Clock = mock[Clock].smart + lazy val clock: Clock = mockSmart[Clock] /** * The settings. diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticatorSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticatorSpec.scala index 97aae2c0..c6b807cf 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticatorSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/authenticators/SessionAuthenticatorSpec.scala @@ -25,12 +25,13 @@ import io.github.honeycombcheesecake.play.silhouette.api.util.{ Clock, Fingerpri import io.github.honeycombcheesecake.play.silhouette.api.{ Authenticator, LoginInfo } import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.SessionAuthenticator._ import io.github.honeycombcheesecake.play.silhouette.impl.authenticators.SessionAuthenticatorService._ -import org.specs2.control.NoLanguageFeatures -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.json.Json import play.api.mvc._ import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.{ mockSmart, mock } import java.time.ZonedDateTime import scala.concurrent.ExecutionContext.Implicits.global @@ -41,7 +42,7 @@ import scala.language.postfixOps /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.authenticators.SessionAuthenticator]]. */ -class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLanguageFeatures { +class SessionAuthenticatorSpec extends PlaySpecification { "The `isValid` method of the authenticator" should { "return false if the authenticator is expired" in new Context { @@ -62,25 +63,31 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan "The `unserialize` method of the authenticator" should { "throw an AuthenticatorException if the given value can't be parsed as Json" in new WithApplication with Context { - val value = "invalid" - val msg = Pattern.quote(JsonParseError.format(ID, value)) + override def running() = { + val value = "invalid" + val msg = Pattern.quote(JsonParseError.format(ID, value)) - unserialize(authenticatorEncoder.encode(value), authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + unserialize(authenticatorEncoder.encode(value), authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + } } "throw an AuthenticatorException if the given value is in the wrong Json format" in new WithApplication with Context { - val value = "{}" - val msg = "^" + Pattern.quote(InvalidJsonFormat.format(ID, "")) + ".*" + override def running() = { + val value = "{}" + val msg = "^" + Pattern.quote(InvalidJsonFormat.format(ID, "")) + ".*" - unserialize(authenticatorEncoder.encode(value), authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + unserialize(authenticatorEncoder.encode(value), authenticatorEncoder) must beFailedTry.withThrowable[AuthenticatorException](msg) + } } } "The `serialize/unserialize` method of the authenticator" should { "serialize/unserialize an authenticator" in new WithApplication with Context { - val value = serialize(authenticator, authenticatorEncoder) + override def running() = { + val value = serialize(authenticator, authenticatorEncoder) - unserialize(value, authenticatorEncoder) must beSuccessfulTry.withValue(authenticator) + unserialize(value, authenticatorEncoder) must beSuccessfulTry.withValue(authenticator) + } } } @@ -88,9 +95,9 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan "return a fingerprinted authenticator" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - clock.now returns ZonedDateTime.now - fingerprintGenerator.generate(any) returns "test" - settings.useFingerprinting returns true + when(clock.now).thenReturn(ZonedDateTime.now) + when(fingerprintGenerator.generate(any)).thenReturn("test") + when(settings.useFingerprinting).thenReturn(true) await(service.create(loginInfo)).fingerprint must beSome("test") } @@ -98,8 +105,8 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan "return a non fingerprinted authenticator" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - clock.now returns ZonedDateTime.now - settings.useFingerprinting returns false + when(clock.now).thenReturn(ZonedDateTime.now) + when(settings.useFingerprinting).thenReturn(false) await(service.create(loginInfo)).fingerprint must beNone } @@ -108,7 +115,7 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - clock.now returns now + when(clock.now).thenReturn(now) await(service.create(loginInfo)).lastUsedDateTime must be equalTo now } @@ -117,7 +124,7 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val now = ZonedDateTime.now - clock.now returns now + when(clock.now).thenReturn(now) await(service.create(loginInfo)).expirationDateTime must be equalTo now + 12.hours } @@ -127,8 +134,8 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan val sixHours = 6 hours val now = ZonedDateTime.now - clock.now returns now - settings.authenticatorExpiry returns sixHours + when(clock.now).thenReturn(now) + when(settings.authenticatorExpiry).thenReturn(sixHours) await(service.create(loginInfo)).expirationDateTime must be equalTo now + sixHours } @@ -136,7 +143,7 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan "throws an AuthenticatorCreationException exception if an error occurred during creation" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - clock.now throws new RuntimeException("Could not get date") + when(clock.now).thenThrow(new RuntimeException("Could not get date")) await(service.create(loginInfo)) must throwA[AuthenticatorCreationException].like { case e => @@ -153,216 +160,260 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan } "return None if session contains invalid json" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode("{")) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode("{")) - settings.useFingerprinting returns false + when(settings.useFingerprinting).thenReturn(false) - await(service.retrieve) must beNone + await(service.retrieve) must beNone + } } "return None if session contains valid json but invalid authenticator" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode("{ \"test\": \"test\" }")) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode("{ \"test\": \"test\" }")) - settings.useFingerprinting returns false + when(settings.useFingerprinting).thenReturn(false) - await(service.retrieve) must beNone + await(service.retrieve) must beNone + } } "return None if authenticator fingerprint doesn't match current fingerprint" in new WithApplication with Context { - fingerprintGenerator.generate(any) returns "false" - settings.useFingerprinting returns true - authenticator.fingerprint returns Some("test") + override def running() = { + when(fingerprintGenerator.generate(any)).thenReturn("false") + when(settings.useFingerprinting).thenReturn(true) + when(authenticator.fingerprint).thenReturn(Some("test")) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) - await(service.retrieve) must beNone + await(service.retrieve) must beNone + } } "return authenticator if authenticator fingerprint matches current fingerprint" in new WithApplication with Context { - fingerprintGenerator.generate(any) returns "test" - settings.useFingerprinting returns true - authenticator.fingerprint returns Some("test") + override def running() = { + when(fingerprintGenerator.generate(any)).thenReturn("test") + when(settings.useFingerprinting).thenReturn(true) + when(authenticator.fingerprint).thenReturn(Some("test")) - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) - await(service.retrieve) must beSome(authenticator) + await(service.retrieve) must beSome(authenticator) + } } "return authenticator if fingerprinting is disabled" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) - settings.useFingerprinting returns false + when(settings.useFingerprinting).thenReturn(false) - await(service.retrieve) must beSome(authenticator) + await(service.retrieve) must beSome(authenticator) + } } "decode an authenticator" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - .withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + .withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) - settings.useFingerprinting returns false + when(settings.useFingerprinting).thenReturn(false) - await(service.retrieve) must beSome(authenticator) + await(service.retrieve) must beSome(authenticator) + } } "throws an AuthenticatorRetrievalException exception if an error occurred during retrieval" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> authenticatorEncoder.encode(Json.toJson(authenticator).toString())) - fingerprintGenerator.generate(any) throws new RuntimeException("Could not generate fingerprint") - settings.useFingerprinting returns true + when(fingerprintGenerator.generate(any)).thenThrow(new RuntimeException("Could not generate fingerprint")) + when(settings.useFingerprinting).thenReturn(true) - await(service.retrieve) must throwA[AuthenticatorRetrievalException].like { - case e => - e.getMessage must startWith(RetrieveError.format(ID, "")) + await(service.retrieve) must throwA[AuthenticatorRetrievalException].like { + case e => + e.getMessage must startWith(RetrieveError.format(ID, "")) + } } } } "The `init` method of the service" should { "return a session with an encoded authenticator" in new WithApplication with AppContext { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val session = await(service.init(authenticator)) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val session = await(service.init(authenticator)) - session must be equalTo sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)) + session must be equalTo sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)) + } } "override existing authenticator from request" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") - val session = await(service.init(authenticator)) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") + val session = await(service.init(authenticator)) - unserialize(session.get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator + unserialize(session.get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator + } } "keep non authenticator related session data" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("test" -> "test") - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val session = await(service.init(authenticator)) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("test" -> "test") + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val session = await(service.init(authenticator)) - session.get(settings.sessionKey) should beSome(data) - session.get("test") should beSome("test") + session.get(settings.sessionKey) should beSome(data) + session.get("test") should beSome("test") + } } } "The result `embed` method of the service" should { "return the response with the session" in new WithApplication with AppContext { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val result = service.embed(sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)), Results.Ok) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val result = service.embed(sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)), Results.Ok) - session(result).get(settings.sessionKey) should beSome(data) + session(result).get(settings.sessionKey) should beSome(data) + } } "override existing authenticator from request" in new WithApplication with AppContext { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val result = service.embed(sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)), Results.Ok) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val result = service.embed(sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)), Results.Ok) - session(result).get(settings.sessionKey) should beSome(data) + session(result).get(settings.sessionKey) should beSome(data) + } } "keep non authenticator related session data" in new WithApplication with AppContext { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep") - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val result = service.embed(sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)), Results.Ok.addingToSession( - "result-other" -> "keep")) - - session(result).get(settings.sessionKey) should beSome(data) - session(result).get("request-other") should beSome("keep") - session(result).get("result-other") should beSome("keep") + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep") + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val result = service.embed(sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)), Results.Ok.addingToSession( + "result-other" -> "keep")) + + session(result).get(settings.sessionKey) should beSome(data) + session(result).get("request-other") should beSome("keep") + session(result).get("result-other") should beSome("keep") + } } } "The request `embed` method of the service" should { "return the request with the session" in new WithApplication with AppContext { - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)) - val request = service.embed(session, FakeRequest()) + override def running() = { + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)) + val request = service.embed(session, FakeRequest()) - request.session.get(settings.sessionKey) should beSome(data) + request.session.get(settings.sessionKey) should beSome(data) + } } "override an existing session" in new WithApplication with AppContext { - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)) - val request = service.embed(session, FakeRequest().withSession(settings.sessionKey -> "test")) + override def running() = { + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> data)) + val request = service.embed(session, FakeRequest().withSession(settings.sessionKey -> "test")) - request.session.get(settings.sessionKey) should beSome(data) + request.session.get(settings.sessionKey) should beSome(data) + } } "should not remove an existing session key" in new WithApplication with AppContext { - val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> "test")) - val request = service.embed(session, FakeRequest().withSession("existing" -> "test")) + override def running() = { + val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> "test")) + val request = service.embed(session, FakeRequest().withSession("existing" -> "test")) - request.session.get("existing") should beSome("test") - request.session.get(settings.sessionKey) should beSome("test") + request.session.get("existing") should beSome("test") + request.session.get(settings.sessionKey) should beSome("test") + } } "keep other request parts" in new WithApplication with AppContext { - val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> "test")) - val request = service.embed(session, FakeRequest().withCookies(Cookie("test", "test"))) - - request.session.get(settings.sessionKey) should beSome("test") - request.cookies.get("test") should beSome[Cookie].which { c => - c.name must be equalTo "test" - c.value must be equalTo "test" + override def running() = { + val session = sessionCookieBaker.deserialize(Map(settings.sessionKey -> "test")) + val request = service.embed(session, FakeRequest().withCookies(Cookie("test", "test"))) + + request.session.get(settings.sessionKey) should beSome("test") + request.cookies.get("test") should beSome[Cookie].which { c => + c.name must be equalTo "test" + c.value must be equalTo "test" + } } } } "The `touch` method of the service" should { "update the last used date if idle timeout is defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns Some(1 second) - clock.now returns ZonedDateTime.now - - service.touch(authenticator) must beLeft[SessionAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo clock.now + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(Some(1 second)) + when(clock.now).thenReturn(ZonedDateTime.now) + + service.touch(authenticator) must beLeft[SessionAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo clock.now + } } } "do not update the last used date if idle timeout is not defined" in new WithApplication with Context { - settings.authenticatorIdleTimeout returns None - clock.now returns ZonedDateTime.now - - service.touch(authenticator) must beRight[SessionAuthenticator].like { - case a => - a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + override def running() = { + when(settings.authenticatorIdleTimeout).thenReturn(None) + when(clock.now).thenReturn(ZonedDateTime.now) + + service.touch(authenticator) must beRight[SessionAuthenticator].like { + case a => + a.lastUsedDateTime must be equalTo authenticator.lastUsedDateTime + } } } } "The `update` method of the service" should { "update the session" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) - val result = service.update(authenticator, Results.Ok) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val data = authenticatorEncoder.encode(Json.toJson(authenticator).toString()) + val result = service.update(authenticator, Results.Ok) - status(result) must be equalTo OK - session(result).get(settings.sessionKey) should beSome(data) + status(result) must be equalTo OK + session(result).get(settings.sessionKey) should beSome(data) + } } "override existing authenticator from request" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") - val result = service.update(authenticator, Results.Ok) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") + val result = service.update(authenticator, Results.Ok) - unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator + unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator + } } "non authenticator related session data" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep") - val result = service.update(authenticator, Results.Ok.addingToSession( - "result-other" -> "keep")) - - unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator - session(result).get("request-other") should beSome("keep") - session(result).get("result-other") should beSome("keep") + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep") + val result = service.update(authenticator, Results.Ok.addingToSession( + "result-other" -> "keep")) + + unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator + session(result).get("request-other") should beSome("keep") + session(result).get("result-other") should beSome("keep") + } } "throws an AuthenticatorUpdateException exception if an error occurred during update" in new Context { implicit val request: FakeRequest[AnyContentAsEmpty.type] = spy(FakeRequest()) - request.session throws new RuntimeException("Cannot get session") + when(request.session).thenThrow(new RuntimeException("Cannot get session")) await(service.update(authenticator, Results.Ok)) must throwA[AuthenticatorUpdateException].like { case e => @@ -373,49 +424,55 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan "The `renew` method of the service" should { "renew the session" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val now = ZonedDateTime.now - val data = authenticatorEncoder.encode(Json.toJson(authenticator.copy( - lastUsedDateTime = now, - expirationDateTime = now + settings.authenticatorExpiry)).toString()) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val now = ZonedDateTime.now + val data = authenticatorEncoder.encode(Json.toJson(authenticator.copy( + lastUsedDateTime = now, + expirationDateTime = now + settings.authenticatorExpiry)).toString()) - settings.useFingerprinting returns false - clock.now returns now + when(settings.useFingerprinting).thenReturn(false) + when(clock.now).thenReturn(now) - val result = service.renew(authenticator, Results.Ok) + val result = service.renew(authenticator, Results.Ok) - session(result).get(settings.sessionKey) should beSome(data) + session(result).get(settings.sessionKey) should beSome(data) + } } "override existing authenticator from request" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") - val now = ZonedDateTime.now + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession(settings.sessionKey -> "existing") + val now = ZonedDateTime.now - settings.useFingerprinting returns false - clock.now returns now + when(settings.useFingerprinting).thenReturn(false) + when(clock.now).thenReturn(now) - val result = service.renew(authenticator, Results.Ok) + val result = service.renew(authenticator, Results.Ok) - unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator.copy( - lastUsedDateTime = now, - expirationDateTime = now + settings.authenticatorExpiry) + unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator.copy( + lastUsedDateTime = now, + expirationDateTime = now + settings.authenticatorExpiry) + } } "non authenticator related session data" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep") - val now = ZonedDateTime.now + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep") + val now = ZonedDateTime.now - settings.useFingerprinting returns false - clock.now returns now + when(settings.useFingerprinting).thenReturn(false) + when(clock.now).thenReturn(now) - val result = service.renew(authenticator, Results.Ok.addingToSession( - "result-other" -> "keep")) + val result = service.renew(authenticator, Results.Ok.addingToSession( + "result-other" -> "keep")) - unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator.copy( - lastUsedDateTime = now, - expirationDateTime = now + settings.authenticatorExpiry) - session(result).get("request-other") should beSome("keep") - session(result).get("result-other") should beSome("keep") + unserialize(session(result).get(settings.sessionKey).get, authenticatorEncoder).get must be equalTo authenticator.copy( + lastUsedDateTime = now, + expirationDateTime = now + settings.authenticatorExpiry) + session(result).get("request-other") should beSome("keep") + session(result).get("result-other") should beSome("keep") + } } "throws an AuthenticatorRenewalException exception if an error occurred during renewal" in new Context { @@ -423,9 +480,9 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan val now = ZonedDateTime.now val okResult = (_: Authenticator) => Future.successful(Results.Ok) - request.session throws new RuntimeException("Cannot get session") - settings.useFingerprinting returns false - clock.now returns now + when(request.session).thenThrow(new RuntimeException("Cannot get session")) + when(settings.useFingerprinting).thenReturn(false) + when(clock.now).thenReturn(now) await(service.renew(authenticator, Results.Ok)) must throwA[AuthenticatorRenewalException].like { case e => @@ -436,32 +493,38 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan "The `discard` method of the service" should { "discard the authenticator from session" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val result = service.discard(authenticator, Results.Ok.withSession( - settings.sessionKey -> "test")) + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val result = service.discard(authenticator, Results.Ok.withSession( + settings.sessionKey -> "test")) - session(result).get(settings.sessionKey) should beNone + session(result).get(settings.sessionKey) should beNone + } } "non authenticator related session data" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep", settings.sessionKey -> "test") - val result = service.discard(authenticator, Results.Ok.addingToSession( - "result-other" -> "keep")) - - session(result).get(settings.sessionKey) should beNone - session(result).get("request-other") should beSome("keep") - session(result).get("result-other") should beSome("keep") + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withSession("request-other" -> "keep", settings.sessionKey -> "test") + val result = service.discard(authenticator, Results.Ok.addingToSession( + "result-other" -> "keep")) + + session(result).get(settings.sessionKey) should beNone + session(result).get("request-other") should beSome("keep") + session(result).get("result-other") should beSome("keep") + } } "throws an AuthenticatorDiscardingException exception if an error occurred during discarding" in new WithApplication with Context { - implicit val request: FakeRequest[AnyContentAsEmpty.type] = spy(FakeRequest()).withSession(settings.sessionKey -> "test") - val result = mock[Result] + override def running() = { + implicit val request: FakeRequest[AnyContentAsEmpty.type] = spy(FakeRequest()).withSession(settings.sessionKey -> "test") + val result = mock[Result] - result.removingFromSession(any)(any) throws new RuntimeException("Cannot get session") + when(result.removingFromSession(any)(any)).thenThrow(new RuntimeException("Cannot get session")) - await(service.discard(authenticator, result)) must throwA[AuthenticatorDiscardingException].like { - case e => - e.getMessage must startWith(DiscardError.format(ID, "")) + await(service.discard(authenticator, result)) must throwA[AuthenticatorDiscardingException].like { + case e => + e.getMessage must startWith(DiscardError.format(ID, "")) + } } } } @@ -474,7 +537,7 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan /** * The ID generator implementation. */ - lazy val fingerprintGenerator = mock[FingerprintGenerator].smart + lazy val fingerprintGenerator = mockSmart[FingerprintGenerator] /** * The authenticator encoder implementation. @@ -484,7 +547,7 @@ class SessionAuthenticatorSpec extends PlaySpecification with Mockito with NoLan /** * The clock implementation. */ - lazy val clock = mock[Clock].smart + lazy val clock = mockSmart[Clock] /** * The settings. diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/BasicAuthProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/BasicAuthProviderSpec.scala index 48f94168..5a138f4a 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/BasicAuthProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/BasicAuthProviderSpec.scala @@ -21,6 +21,7 @@ import io.github.honeycombcheesecake.play.silhouette.api.exceptions._ import io.github.honeycombcheesecake.play.silhouette.api.util._ import io.github.honeycombcheesecake.play.silhouette.impl.providers.PasswordProvider._ import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -32,103 +33,123 @@ class BasicAuthProviderSpec extends PasswordProviderSpec { "The `authenticate` method" should { "throw ConfigurationException if unsupported hasher is stored" in new WithApplication with Context { - val passwordInfo = PasswordInfo("unknown", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) - val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) + override def running() = { + val passwordInfo = PasswordInfo("unknown", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) + val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) - await(provider.authenticate(request)) must throwA[ConfigurationException].like { - case e => e.getMessage must beEqualTo(HasherIsNotRegistered.format(provider.id, "unknown", "foo, bar")) + await(provider.authenticate(request)) must throwA[ConfigurationException].like { + case e => e.getMessage must beEqualTo(HasherIsNotRegistered.format(provider.id, "unknown", "foo, bar")) + } } } "return None if no auth info could be found for the given credentials" in new WithApplication with Context { - val loginInfo = new LoginInfo(provider.id, credentials.identifier) - val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) + override def running() = { + val loginInfo = new LoginInfo(provider.id, credentials.identifier) + val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(None) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(None)) - await(provider.authenticate(request)) must beNone + await(provider.authenticate(request)) must beNone + } } "return None if password does not match" in new WithApplication with Context { - val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) - val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) + override def running() = { + val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) + val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) - fooHasher.matches(passwordInfo, credentials.password) returns false - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) + when(fooHasher.matches(passwordInfo, credentials.password)).thenReturn(false) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) - await(provider.authenticate(request)) must beNone + await(provider.authenticate(request)) must beNone + } } "return None if provider isn't responsible" in new WithApplication with Context { - await(provider.authenticate(FakeRequest())) must beNone + override def running() = { + await(provider.authenticate(FakeRequest())) must beNone + } } "return None for wrong encoded credentials" in new WithApplication with Context { - val request = FakeRequest().withHeaders(AUTHORIZATION -> "wrong") + override def running() = { + val request = FakeRequest().withHeaders(AUTHORIZATION -> "wrong") - await(provider.authenticate(request)) must beNone + await(provider.authenticate(request)) must beNone + } } "return login info if passwords does match" in new WithApplication with Context { - val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) - val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) + override def running() = { + val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) + val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) - fooHasher.matches(passwordInfo, credentials.password) returns true - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) + when(fooHasher.matches(passwordInfo, credentials.password)).thenReturn(true) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) - await(provider.authenticate(request)) must beSome(loginInfo) + await(provider.authenticate(request)) must beSome(loginInfo) + } } "handle a colon in a password" in new WithApplication with Context { - val credentialsWithColon = Credentials("apollonia.vanova@watchmen.com", "s3c:r3t") - val passwordInfo = PasswordInfo("foo", "hashed(s3c:r3t)") - val loginInfo = LoginInfo(provider.id, credentialsWithColon.identifier) - val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentialsWithColon)) + override def running() = { + val credentialsWithColon = Credentials("apollonia.vanova@watchmen.com", "s3c:r3t") + val passwordInfo = PasswordInfo("foo", "hashed(s3c:r3t)") + val loginInfo = LoginInfo(provider.id, credentialsWithColon.identifier) + val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentialsWithColon)) - fooHasher.matches(passwordInfo, credentialsWithColon.password) returns true - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) + when(fooHasher.matches(passwordInfo, credentialsWithColon.password)).thenReturn(true) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) - await(provider.authenticate(request)) must beSome(loginInfo) + await(provider.authenticate(request)) must beSome(loginInfo) + } } "re-hash password with new hasher if hasher is deprecated" in new WithApplication with Context { - val passwordInfo = PasswordInfo("bar", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) - val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) - - fooHasher.hash(credentials.password) returns passwordInfo - barHasher.matches(passwordInfo, credentials.password) returns true - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) - authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo) returns Future.successful(passwordInfo) - - await(provider.authenticate(request)) must beSome(loginInfo) - there was one(authInfoRepository).update(loginInfo, passwordInfo) + override def running() = { + val passwordInfo = PasswordInfo("bar", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) + val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) + + when(fooHasher.hash(credentials.password)).thenReturn(passwordInfo) + when(barHasher.matches(passwordInfo, credentials.password)).thenReturn(true) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) + when(authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo)).thenReturn(Future.successful(passwordInfo)) + + await(provider.authenticate(request)) must beSome(loginInfo) + verify(authInfoRepository).update(loginInfo, passwordInfo) + } } "re-hash password with new hasher if password info is deprecated" in new WithApplication with Context { - val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) - val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) - - fooHasher.isDeprecated(passwordInfo) returns Some(true) - fooHasher.hash(credentials.password) returns passwordInfo - fooHasher.matches(passwordInfo, credentials.password) returns true - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) - authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo) returns Future.successful(passwordInfo) - - await(provider.authenticate(request)) must beSome(loginInfo) - there was one(authInfoRepository).update(loginInfo, passwordInfo) + override def running() = { + val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) + val request = FakeRequest().withHeaders(AUTHORIZATION -> encodeCredentials(credentials)) + + when(fooHasher.isDeprecated(passwordInfo)).thenReturn(Some(true)) + when(fooHasher.hash(credentials.password)).thenReturn(passwordInfo) + when(fooHasher.matches(passwordInfo, credentials.password)).thenReturn(true) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) + when(authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo)).thenReturn(Future.successful(passwordInfo)) + + await(provider.authenticate(request)) must beSome(loginInfo) + verify(authInfoRepository).update(loginInfo, passwordInfo) + } } "return None if Authorization method is not Basic and Base64 decoded header has ':'" in new WithApplication with Context { - val request = FakeRequest().withHeaders(AUTHORIZATION -> Base64.encode("NotBasic foo:bar")) + override def running() = { + val request = FakeRequest().withHeaders(AUTHORIZATION -> Base64.encode("NotBasic foo:bar")) - await(provider.authenticate(request)) must beNone + await(provider.authenticate(request)) must beNone + } } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/CredentialsProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/CredentialsProviderSpec.scala index 93ca5b6b..72065b2d 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/CredentialsProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/CredentialsProviderSpec.scala @@ -21,6 +21,7 @@ import io.github.honeycombcheesecake.play.silhouette.api.util.{ Credentials, Pas import io.github.honeycombcheesecake.play.silhouette.impl.exceptions.{ IdentityNotFoundException, InvalidPasswordException } import io.github.honeycombcheesecake.play.silhouette.impl.providers.PasswordProvider._ import play.api.test.WithApplication +import org.mockito.Mockito._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -32,73 +33,85 @@ class CredentialsProviderSpec extends PasswordProviderSpec { "The `authenticate` method" should { "throw IdentityNotFoundException if no auth info could be found for the given credentials" in new WithApplication with Context { - val loginInfo = new LoginInfo(provider.id, credentials.identifier) + override def running() = { + val loginInfo = new LoginInfo(provider.id, credentials.identifier) - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(None) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(None)) - await(provider.authenticate(credentials)) must throwA[IdentityNotFoundException].like { - case e => e.getMessage must beEqualTo(PasswordInfoNotFound.format(provider.id, loginInfo)) + await(provider.authenticate(credentials)) must throwA[IdentityNotFoundException].like { + case e => e.getMessage must beEqualTo(PasswordInfoNotFound.format(provider.id, loginInfo)) + } } } "throw InvalidPasswordException if password does not match" in new WithApplication with Context { - val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) + override def running() = { + val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) - fooHasher.matches(passwordInfo, credentials.password) returns false - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) + when(fooHasher.matches(passwordInfo, credentials.password)).thenReturn(false) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) - await(provider.authenticate(credentials)) must throwA[InvalidPasswordException].like { - case e => e.getMessage must beEqualTo(PasswordDoesNotMatch.format(provider.id)) + await(provider.authenticate(credentials)) must throwA[InvalidPasswordException].like { + case e => e.getMessage must beEqualTo(PasswordDoesNotMatch.format(provider.id)) + } } } "throw ConfigurationException if unsupported hasher is stored" in new WithApplication with Context { - val passwordInfo = PasswordInfo("unknown", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) + override def running() = { + val passwordInfo = PasswordInfo("unknown", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) - await(provider.authenticate(credentials)) must throwA[ConfigurationException].like { - case e => e.getMessage must beEqualTo(HasherIsNotRegistered.format(provider.id, "unknown", "foo, bar")) + await(provider.authenticate(credentials)) must throwA[ConfigurationException].like { + case e => e.getMessage must beEqualTo(HasherIsNotRegistered.format(provider.id, "unknown", "foo, bar")) + } } } "return login info if passwords does match" in new WithApplication with Context { - val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) + override def running() = { + val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) - fooHasher.matches(passwordInfo, credentials.password) returns true - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) + when(fooHasher.matches(passwordInfo, credentials.password)).thenReturn(true) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) - await(provider.authenticate(credentials)) must be equalTo loginInfo + await(provider.authenticate(credentials)) must be equalTo loginInfo + } } "re-hash password with new hasher if hasher is deprecated" in new WithApplication with Context { - val passwordInfo = PasswordInfo("bar", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) + override def running() = { + val passwordInfo = PasswordInfo("bar", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) - fooHasher.hash(credentials.password) returns passwordInfo - barHasher.matches(passwordInfo, credentials.password) returns true - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) - authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo) returns Future.successful(passwordInfo) + when(fooHasher.hash(credentials.password)).thenReturn(passwordInfo) + when(barHasher.matches(passwordInfo, credentials.password)).thenReturn(true) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) + when(authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo)).thenReturn(Future.successful(passwordInfo)) - await(provider.authenticate(credentials)) must be equalTo loginInfo - there was one(authInfoRepository).update(loginInfo, passwordInfo) + await(provider.authenticate(credentials)) must be equalTo loginInfo + verify(authInfoRepository).update(loginInfo, passwordInfo) + } } "re-hash password with new hasher if hasher is deprecated" in new WithApplication with Context { - val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") - val loginInfo = LoginInfo(provider.id, credentials.identifier) - - fooHasher.isDeprecated(passwordInfo) returns Some(true) - fooHasher.hash(credentials.password) returns passwordInfo - fooHasher.matches(passwordInfo, credentials.password) returns true - authInfoRepository.find[PasswordInfo](loginInfo) returns Future.successful(Some(passwordInfo)) - authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo) returns Future.successful(passwordInfo) - - await(provider.authenticate(credentials)) must be equalTo loginInfo - there was one(authInfoRepository).update(loginInfo, passwordInfo) + override def running() = { + val passwordInfo = PasswordInfo("foo", "hashed(s3cr3t)") + val loginInfo = LoginInfo(provider.id, credentials.identifier) + + when(fooHasher.isDeprecated(passwordInfo)).thenReturn(Some(true)) + when(fooHasher.hash(credentials.password)).thenReturn(passwordInfo) + when(fooHasher.matches(passwordInfo, credentials.password)).thenReturn(true) + when(authInfoRepository.find[PasswordInfo](loginInfo)).thenReturn(Future.successful(Some(passwordInfo))) + when(authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo)).thenReturn(Future.successful(passwordInfo)) + + await(provider.authenticate(credentials)) must be equalTo loginInfo + verify(authInfoRepository).update(loginInfo, passwordInfo) + } } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/DefaultSocialStateHandlerSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/DefaultSocialStateHandlerSpec.scala index b6e28e70..053e2611 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/DefaultSocialStateHandlerSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/DefaultSocialStateHandlerSpec.scala @@ -21,11 +21,13 @@ import io.github.honeycombcheesecake.play.silhouette.api.util.ExtractableRequest import io.github.honeycombcheesecake.play.silhouette.impl.providers.DefaultSocialStateHandler._ import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialStateItem.ItemStructure import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.json.Json import play.api.mvc.{ AnyContentAsEmpty, Results } import play.api.test.{ FakeRequest, PlaySpecification } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mockSmart import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -34,11 +36,11 @@ import scala.util.{ Failure, Success } /** * Test case for the [[DefaultSocialStateHandler]] class. */ -class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with JsonMatchers { +class DefaultSocialStateHandlerSpec extends PlaySpecification with JsonMatchers { "The `withHandler` method" should { "return a new state handler with the given item handler added" in new Context { - val newHandler = mock[SocialStateItemHandler].smart + val newHandler = mockSmart[SocialStateItemHandler] stateHandler.handlers.size must be equalTo 2 stateHandler.withHandler(newHandler).handlers.size must be equalTo 3 @@ -47,8 +49,8 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with "The `state` method" should { "return the social state" in new Context { - Default.itemHandler.item returns Future.successful(Default.item) - Publishable.itemHandler.item returns Future.successful(Publishable.item) + when(Default.itemHandler.item).thenReturn(Future.successful(Default.item)) + when(Publishable.itemHandler.item).thenReturn(Future.successful(Publishable.item)) await(stateHandler.state) must be equalTo state } @@ -66,13 +68,13 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with } "return the serialized social state" in new Context { - Default.itemHandler.canHandle(Publishable.item) returns None - Default.itemHandler.canHandle(Default.item) returns Some(Default.item) - Default.itemHandler.serialize(Default.item) returns Default.structure + when(Default.itemHandler.canHandle(Publishable.item)).thenReturn(None) + when(Default.itemHandler.canHandle(Default.item)).thenReturn(Some(Default.item)) + when(Default.itemHandler.serialize(Default.item)).thenReturn(Default.structure) - Publishable.itemHandler.canHandle(Default.item) returns None - Publishable.itemHandler.canHandle(Publishable.item) returns Some(Publishable.item) - Publishable.itemHandler.serialize(Publishable.item) returns Publishable.structure + when(Publishable.itemHandler.canHandle(Default.item)).thenReturn(None) + when(Publishable.itemHandler.canHandle(Publishable.item)).thenReturn(Some(Publishable.item)) + when(Publishable.itemHandler.serialize(Publishable.item)).thenReturn(Publishable.structure) stateHandler.serialize(state) must be equalTo s"${Publishable.structure.asString}.${Default.structure.asString}" } @@ -85,7 +87,7 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with await(stateHandler.unserialize("")) - there was no(signer).extract(any[String]()) + verify(signer, never()).extract(any[String]()) } "throw an Exception for an empty string" in new Context { @@ -111,8 +113,8 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with implicit val request: ExtractableRequest[AnyContentAsEmpty.type] = new ExtractableRequest(FakeRequest()) val serialized = s"${Default.structure.asString}" - Default.itemHandler.canHandle(any[ItemStructure]())(any()) returns false - Publishable.itemHandler.canHandle(any[ItemStructure]())(any()) returns false + when(Default.itemHandler.canHandle(any[ItemStructure]())(any())).thenReturn(false) + when(Publishable.itemHandler.canHandle(any[ItemStructure]())(any())).thenReturn(false) await(stateHandler.unserialize(serialized)) must throwA[ProviderException].like { case e => @@ -124,13 +126,13 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with implicit val request: ExtractableRequest[AnyContentAsEmpty.type] = new ExtractableRequest(FakeRequest()) val serialized = s"${Default.structure.asString}.${Publishable.structure.asString}" - Default.itemHandler.canHandle(Publishable.structure) returns false - Default.itemHandler.canHandle(Default.structure) returns true - Default.itemHandler.unserialize(Default.structure) returns Future.successful(Default.item) + when(Default.itemHandler.canHandle(Publishable.structure)).thenReturn(false) + when(Default.itemHandler.canHandle(Default.structure)).thenReturn(true) + when(Default.itemHandler.unserialize(Default.structure)).thenReturn(Future.successful(Default.item)) - Publishable.itemHandler.canHandle(Default.structure) returns false - Publishable.itemHandler.canHandle(Publishable.structure) returns true - Publishable.itemHandler.unserialize(Publishable.structure) returns Future.successful(Publishable.item) + when(Publishable.itemHandler.canHandle(Default.structure)).thenReturn(false) + when(Publishable.itemHandler.canHandle(Publishable.structure)).thenReturn(true) + when(Publishable.itemHandler.unserialize(Publishable.structure)).thenReturn(Future.successful(Publishable.item)) await(stateHandler.unserialize(serialized)) must be equalTo SocialState(Set(Default.item, Publishable.item)) } @@ -142,9 +144,9 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with val result = Results.Ok val publishedResult = Results.Ok.withHeaders("X-PUBLISHED" -> "true") - Publishable.itemHandler.publish(Publishable.item, result) returns publishedResult - Publishable.itemHandler.canHandle(Default.item) returns None - Publishable.itemHandler.canHandle(Publishable.item) returns Some(Publishable.item) + when(Publishable.itemHandler.publish(Publishable.item, result)).thenReturn(publishedResult) + when(Publishable.itemHandler.canHandle(Default.item)).thenReturn(None) + when(Publishable.itemHandler.canHandle(Publishable.item)).thenReturn(Some(Publishable.item)) stateHandler.publish(result, state) must be equalTo publishedResult } @@ -153,8 +155,8 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() val result = Results.Ok - Publishable.itemHandler.canHandle(Default.item) returns None - Publishable.itemHandler.canHandle(Publishable.item) returns None + when(Publishable.itemHandler.canHandle(Default.item)).thenReturn(None) + when(Publishable.itemHandler.canHandle(Publishable.item)).thenReturn(None) stateHandler.publish(result, state) must be equalTo result } @@ -173,7 +175,7 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with type Item = DefaultItem } object Default { - val itemHandler = mock[DefaultItemHandler].smart + val itemHandler = mockSmart[DefaultItemHandler] val item = DefaultItem() val structure = ItemStructure("default", Json.obj()) } @@ -186,7 +188,7 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with type Item = PublishableItem } object Publishable { - val itemHandler = mock[PublishableItemHandler].smart + val itemHandler = mockSmart[PublishableItemHandler] val item = PublishableItem() val structure = ItemStructure("publishable", Json.obj()) } @@ -197,10 +199,10 @@ class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with * The signer returns the same value as passed to the methods. This is enough for testing. */ val signer = { - val c = mock[Signer].smart - c.sign(any()) answers { p: Any => p.asInstanceOf[String] } - c.extract(any()) answers { p: Any => - p.asInstanceOf[String] match { + val c = mockSmart[Signer] + when(c.sign(any())).thenAnswer(_.getArgument(0).asInstanceOf[String]) + when(c.extract(any())).thenAnswer { p => + p.getArgument(0).asInstanceOf[String] match { case "" => Failure(new RuntimeException("Wrong state format")) case s => Success(s) } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth1ProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth1ProviderSpec.scala index 8450b3b4..3228ce6e 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth1ProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth1ProviderSpec.scala @@ -20,12 +20,16 @@ import io.github.honeycombcheesecake.play.silhouette.impl.exceptions.{ AccessDen import io.github.honeycombcheesecake.play.silhouette.impl.providers.OAuth1Provider._ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.services.PlayOAuth1Service import org.specs2.matcher.ThrownExpectations -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.mvc.{ AnyContent, AnyContentAsEmpty, Result, Results } import play.api.test.{ FakeHeaders, FakeRequest, WithApplication } import play.mvc.Http.HeaderNames +import org.hamcrest.core.IsAnything +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ +import org.mockito.hamcrest.MockitoHamcrest import test.SocialProviderSpec +import test.Helper.mock import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -41,100 +45,119 @@ abstract class OAuth1ProviderSpec extends SocialProviderSpec[OAuth1Info] { "The provider" should { val c = context "throw a RuntimeException if the unsafe 1.0 specification should be used" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Denied + "=") - c.oAuthService.use10a returns false - c.provider.authenticate() must throwA[RuntimeException] + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Denied + "=") + when(c.oAuthService.use10a).thenReturn(false) + c.provider.authenticate() must throwA[RuntimeException] + } } } "The authenticate method" should { val c = context "fail with an AccessDeniedException if denied key exists in query string" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Denied + "=") - failed[AccessDeniedException](c.provider.authenticate()) { - case e => e.getMessage must startWith(AuthorizationError.format(c.provider.id, "")) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Denied + "=") + failed[AccessDeniedException](c.provider.authenticate()) { + case e => e.getMessage must startWith(AuthorizationError.format(c.provider.id, "")) + } } } "fail with an UnexpectedResponseException if request token cannot be retrieved" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - c.oAuthService.retrieveRequestToken(c.oAuthSettings.callbackURL) returns Future.failed(new Exception("")) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + when(c.oAuthService.retrieveRequestToken(c.oAuthSettings.callbackURL)).thenReturn(Future.failed(new Exception(""))) - failed[UnexpectedResponseException](c.provider.authenticate()) { - case e => e.getMessage must startWith(ErrorRequestToken.format(c.provider.id, "")) + failed[UnexpectedResponseException](c.provider.authenticate()) { + case e => e.getMessage must startWith(ErrorRequestToken.format(c.provider.id, "")) + } } } "redirect to authorization URL if request token could be retrieved" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val serializedTokenSecret = "my.serialized.token.secret" - - c.oAuthService.retrieveRequestToken(c.oAuthSettings.callbackURL) returns Future.successful(c.oAuthInfo) - c.oAuthService.redirectUrl(any()) answers { _: Any => c.oAuthSettings.authorizationURL } - c.oAuthTokenSecretProvider.build(any())(any(), any()) returns Future.successful(c.oAuthTokenSecret) - c.oAuthTokenSecretProvider.publish(any(), any())(any()) answers { (a, _) => - a.asInstanceOf[Array[Any]](0).asInstanceOf[Result] - } - - result(c.provider.authenticate()) { result => - status(result) must equalTo(SEE_OTHER) - redirectLocation(result) must beSome.which(_ == c.oAuthSettings.authorizationURL) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val serializedTokenSecret = "my.serialized.token.secret" + + when(c.oAuthService.retrieveRequestToken(c.oAuthSettings.callbackURL)).thenReturn(Future.successful(c.oAuthInfo)) + when(c.oAuthService.redirectUrl(any())).thenAnswer(_ => c.oAuthSettings.authorizationURL) + when(c.oAuthTokenSecretProvider.build(any())(any(), any())).thenReturn(Future.successful(c.oAuthTokenSecret)) + // when(c.oAuthTokenSecretProvider.publish(any(), any())(any())).thenAnswer(_.getArguments.asInstanceOf[Array[Any]](0).asInstanceOf[Result]) + when(c.oAuthTokenSecretProvider.publish(any(), any())(any())).thenAnswer(_.getArgument(0).asInstanceOf[Result]) + + result(c.provider.authenticate()) { result => + status(result) must equalTo(SEE_OTHER) + redirectLocation(result) must beSome[String].which(_ == c.oAuthSettings.authorizationURL) + } } } "resolves relative redirectURLs before starting the flow" in new WithApplication { - verifyCallbackURLResolution("/callback-url", secure = false, "http://www.example.com/callback-url") + override def running() = { + verifyCallbackURLResolution("/callback-url", secure = false, "http://www.example.com/callback-url") + } } "resolves path relative redirectURLS before starting the flow" in new WithApplication { - verifyCallbackURLResolution("callback-url", secure = false, "http://www.example.com/request-path/callback-url") - } - - "resolves relative redirectURLs before starting the flow over https" in new WithApplication { - verifyCallbackURLResolution("/callback-url", secure = true, "https://www.example.com/callback-url") + override def running() = { + verifyCallbackURLResolution("callback-url", secure = false, "http://www.example.com/request-path/callback-url") + } } def verifyCallbackURLResolution(callbackURL: String, secure: Boolean, resolvedCallbackURL: String) = { implicit val req = FakeRequest[AnyContent]( method = GET, uri = "/request-path/something", - headers = FakeHeaders(Seq(HeaderNames.HOST -> "www.example.com")), + headers = FakeHeaders(Seq((HeaderNames.HOST, "www.example.com"))), body = AnyContentAsEmpty, secure = secure) - c.oAuthSettings.callbackURL returns callbackURL + when(c.oAuthSettings.callbackURL).thenReturn(callbackURL) - c.oAuthService.retrieveRequestToken(any())(any()) returns Future.successful(c.oAuthInfo) - c.oAuthService.redirectUrl(c.oAuthInfo.token) returns c.oAuthSettings.authorizationURL - c.oAuthTokenSecretProvider.build(any())(any(), any()) returns Future.successful(c.oAuthTokenSecret) - c.oAuthTokenSecretProvider.publish(any(), any())(any()) answers { _: Any => Results.Redirect(c.oAuthSettings.authorizationURL) } + when(c.oAuthService.retrieveRequestToken(any())(any())).thenReturn(Future.successful(c.oAuthInfo)) + + when(c.oAuthService.redirectUrl(c.oAuthInfo.token)).thenAnswer(_ => c.oAuthSettings.authorizationURL) + + when(c.oAuthTokenSecretProvider.build(any())(any(), any())).thenReturn(Future.successful(c.oAuthTokenSecret)) + when(c.oAuthTokenSecretProvider.publish(any(), any())(any())).thenAnswer(_ => Results.Redirect(c.oAuthSettings.authorizationURL)) await(c.provider.authenticate()) - there was one(c.oAuthService).retrieveRequestToken(resolvedCallbackURL) + verify(c.oAuthService).retrieveRequestToken(resolvedCallbackURL) + } + + "resolves relative redirectURLs before starting the flow over https" in new WithApplication { + override def running() = { + verifyCallbackURLResolution("/callback-url", secure = true, "https://www.example.com/callback-url") + } } "fail with an UnexpectedResponseException if access token cannot be retrieved" in new WithApplication { - val tokenSecret = "my.token.secret" - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + OAuthVerifier + "=my.verifier&" + OAuthToken + "=my.token") + override def running() = { + val tokenSecret = "my.token.secret" + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + OAuthVerifier + "=my.verifier&" + OAuthToken + "=my.token") - c.oAuthTokenSecret.value returns tokenSecret - c.oAuthTokenSecretProvider.retrieve(any(), any()) returns Future.successful(c.oAuthTokenSecret) - c.oAuthService.retrieveAccessToken(c.oAuthInfo.copy(secret = tokenSecret), "my.verifier") returns Future.failed(new Exception("")) + when(c.oAuthTokenSecret.value).thenReturn(tokenSecret) + when(c.oAuthTokenSecretProvider.retrieve(any(), any())).thenReturn(Future.successful(c.oAuthTokenSecret)) + when(c.oAuthService.retrieveAccessToken(c.oAuthInfo.copy(secret = tokenSecret), "my.verifier")).thenReturn(Future.failed(new Exception(""))) - failed[UnexpectedResponseException](c.provider.authenticate()) { - case e => e.getMessage must startWith(ErrorAccessToken.format(c.provider.id, "")) + failed[UnexpectedResponseException](c.provider.authenticate()) { + case e => e.getMessage must startWith(ErrorAccessToken.format(c.provider.id, "")) + } } } "return the auth info" in new WithApplication { - val tokenSecret = "my.token.secret" - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + OAuthVerifier + "=my.verifier&" + OAuthToken + "=my.token") + override def running() = { + val tokenSecret = "my.token.secret" + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + OAuthVerifier + "=my.verifier&" + OAuthToken + "=my.token") - c.oAuthTokenSecret.value returns tokenSecret - c.oAuthTokenSecretProvider.retrieve(any(), any()) returns Future.successful(c.oAuthTokenSecret) - c.oAuthService.retrieveAccessToken(c.oAuthInfo.copy(secret = tokenSecret), "my.verifier") returns Future.successful(c.oAuthInfo) + when(c.oAuthTokenSecret.value).thenReturn(tokenSecret) + when(c.oAuthTokenSecretProvider.retrieve(any(), any())).thenReturn(Future.successful(c.oAuthTokenSecret)) + when(c.oAuthService.retrieveAccessToken(c.oAuthInfo.copy(secret = tokenSecret), "my.verifier")).thenReturn(Future.successful(c.oAuthInfo)) - authInfo(c.provider.authenticate())(_ must be equalTo c.oAuthInfo) + authInfo(c.provider.authenticate())(_ must be equalTo c.oAuthInfo) + } } } @@ -156,7 +179,7 @@ abstract class OAuth1ProviderSpec extends SocialProviderSpec[OAuth1Info] { /** * Context for the OAuth1ProviderSpec. */ -trait OAuth1ProviderSpecContext extends Scope with Mockito with ThrownExpectations { +trait OAuth1ProviderSpecContext extends Scope with ThrownExpectations { abstract class TestSecret extends OAuth1TokenSecret abstract class TestStateProvider extends OAuth1TokenSecretProvider { @@ -168,7 +191,7 @@ trait OAuth1ProviderSpecContext extends Scope with Mockito with ThrownExpectatio */ lazy val httpLayer = { val m = mock[MockHTTPLayer] - m.executionContext returns global + when(m.executionContext).thenReturn(global) m } @@ -181,10 +204,11 @@ trait OAuth1ProviderSpecContext extends Scope with Mockito with ThrownExpectatio * The OAuth1 service mock. */ lazy val oAuthService: OAuth1Service = { + val s = mock[PlayOAuth1Service] - s.use10a returns true - s.withSettings(anyFunction1) returns s - s.redirectUrl(anyString) returns "TESTING" + when(s.use10a).thenReturn(true) + when(s.withSettings(MockitoHamcrest.argThat(new IsAnything[OAuth1Settings => OAuth1Settings]))).thenReturn(s) + when(s.redirectUrl(anyString)).thenReturn("TESTING") s } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2ProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2ProviderSpec.scala index 7d386e25..f93dc674 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2ProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OAuth2ProviderSpec.scala @@ -24,13 +24,15 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.OAuth2Provid import io.github.honeycombcheesecake.play.silhouette.impl.providers.state.UserStateItem import io.github.honeycombcheesecake.play.silhouette.helpers.Transform._ import org.specs2.matcher.ThrownExpectations -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.json.{ JsValue, Json } import play.api.mvc.{ AnyContent, AnyContentAsEmpty, Result } import play.api.test.{ FakeHeaders, FakeRequest, WithApplication } import play.mvc.Http.HeaderNames import test.SocialStateProviderSpec +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ +import test.Helper.{ mockSmart, mock } import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ ExecutionContext, Future } @@ -46,103 +48,125 @@ abstract class OAuth2ProviderSpec extends SocialStateProviderSpec[OAuth2Info, So "The `authenticate` method" should { val c = context "fail with an AccessDeniedException if `error` key with value `access_denied` exists in query string" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Error + "=" + AccessDenied) - failed[AccessDeniedException](c.provider.authenticate()) { - case e => e.getMessage must startWith(AuthorizationError.format(c.provider.id, "")) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Error + "=" + AccessDenied) + failed[AccessDeniedException](c.provider.authenticate()) { + case e => e.getMessage must startWith(AuthorizationError.format(c.provider.id, "")) + } } } "fail with an UnexpectedResponseException if `error` key with unspecified value exists in query string" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Error + "=unspecified") - failed[UnexpectedResponseException](c.provider.authenticate()) { - case e => e.getMessage must startWith(AuthorizationError.format(c.provider.id, "unspecified")) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Error + "=unspecified") + failed[UnexpectedResponseException](c.provider.authenticate()) { + case e => e.getMessage must startWith(AuthorizationError.format(c.provider.id, "unspecified")) + } } } "fail with an ConfigurationException if authorization URL is undefined when it's needed" in new WithApplication { - c.oAuthSettings.authorizationURL match { - case None => skipped("authorizationURL is not defined, so this step isn't needed for provider: " + c.provider.getClass) - case Some(_) => - implicit val req = FakeRequest(GET, "/") - - c.stateProvider.serialize(c.state) returns "session-value" - c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.state(any[ExecutionContext]) returns Future.successful(c.state) - c.oAuthSettings.authorizationURL returns None - - failed[ConfigurationException](c.provider.authenticate()) { - case e => e.getMessage must startWith(AuthorizationURLUndefined.format(c.provider.id)) - } + override def running() = { + c.oAuthSettings.authorizationURL match { + case None => skipped("authorizationURL is not defined, so this step isn't needed for provider: " + c.provider.getClass) + case Some(_) => + implicit val req = FakeRequest(GET, "/") + + when(c.stateProvider.serialize(c.state)).thenReturn("session-value") + when(c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.oAuthSettings.authorizationURL).thenReturn(None) + + failed[ConfigurationException](c.provider.authenticate()) { + case e => e.getMessage must startWith(AuthorizationURLUndefined.format(c.provider.id)) + } + } } } "redirect to authorization URL if authorization code doesn't exists in request" in new WithApplication { - c.oAuthSettings.authorizationURL match { - case None => skipped("authorizationURL is not defined, so this step isn't needed for provider: " + c.provider.getClass) - case Some(authorizationURL) => - implicit val req = FakeRequest(GET, "/") - val sessionKey = "session-key" - val sessionValue = "session-value" - - c.stateProvider.serialize(c.state) returns sessionValue - c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.state(any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.publish(any, any)(any) answers { (a, _) => - val result = a.asInstanceOf[Array[Any]](0).asInstanceOf[Result] - val state = a.asInstanceOf[Array[Any]](1).asInstanceOf[c.TestState] - - result.withSession(sessionKey -> c.stateProvider.serialize(state)) - } + override def running() = { + c.oAuthSettings.authorizationURL match { + case None => skipped("authorizationURL is not defined, so this step isn't needed for provider: " + c.provider.getClass) + case Some(authorizationURL) => + implicit val req = FakeRequest(GET, "/") + val sessionKey = "session-key" + val sessionValue = "session-value" + + when(c.stateProvider.serialize(c.state)).thenReturn(sessionValue) + when(c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.publish(any, any)(any)).thenAnswer { m => + val result = m.getArgument(0).asInstanceOf[Result] + val state = m.getArgument(1).asInstanceOf[c.TestState] + + result.withSession(sessionKey -> c.stateProvider.serialize(state)) + } - result(c.provider.authenticate()) { result => - status(result) must equalTo(SEE_OTHER) - session(result).get(sessionKey) must beSome(c.stateProvider.serialize(c.state)) - redirectLocation(result) must beSome.which { url => - val urlParams = c.urlParams(url) - val redirectParam = c.oAuthSettings.redirectURL match { - case Some(rUri) => List((RedirectURI, rUri)) - case None => Nil - } - val params = c.oAuthSettings.scope.foldLeft(List( - (ClientID, c.oAuthSettings.clientID), - (ResponseType, Code), - (State, urlParams(State))) ++ c.oAuthSettings.authorizationParams.toList ++ redirectParam) { - case (p, s) => (Scope, s) :: p + result(c.provider.authenticate()) { result => + status(result) must equalTo(SEE_OTHER) + session(result).get(sessionKey) must beSome(c.stateProvider.serialize(c.state)) + redirectLocation(result) must beSome[String].which { url => + val urlParams = c.urlParams(url) + val redirectParam = c.oAuthSettings.redirectURL match { + case Some(rUri) => List((RedirectURI, rUri)) + case None => Nil + } + val params = c.oAuthSettings.scope.foldLeft(List( + (ClientID, c.oAuthSettings.clientID), + (ResponseType, Code), + (State, urlParams(State))) ++ c.oAuthSettings.authorizationParams.toList ++ redirectParam) { + case (p, s) => (Scope, s) :: p + } + url must be equalTo (authorizationURL + params.map { p => + encode(p._1, "UTF-8") + "=" + encode(p._2, "UTF-8") + }.mkString("?", "&", "")) } - url must be equalTo (authorizationURL + params.map { p => - encode(p._1, "UTF-8") + "=" + encode(p._2, "UTF-8") - }.mkString("?", "&", "")) } - } + } } } "resolves relative redirectURLs before starting the flow" in new WithApplication { - verifyRelativeRedirectResolution("/redirect-url", secure = false, "http://www.example.com/redirect-url") + override def running() = { + verifyRelativeRedirectResolution("/redirect-url", secure = false, "http://www.example.com/redirect-url") + } } "resolves path relative redirectURLs before starting the flow" in new WithApplication { - verifyRelativeRedirectResolution("redirect-url", secure = false, "http://www.example.com/request-path/redirect-url") + override def running() = { + verifyRelativeRedirectResolution("redirect-url", secure = false, "http://www.example.com/request-path/redirect-url") + } } "resolves relative redirectURLs before starting the flow over https" in new WithApplication { - verifyRelativeRedirectResolution("/redirect-url", secure = true, "https://www.example.com/redirect-url") + override def running() = { + verifyRelativeRedirectResolution("/redirect-url", secure = true, "https://www.example.com/redirect-url") + } } "verifying presence of redirect param in the access token post request" in new WithApplication { - verifyPresenceOrAbsenceOfRedirectURL(Some("/redirect-url"), secure = false, "http://www.example.com/redirect-url") + override def running() = { + verifyPresenceOrAbsenceOfRedirectURL(Some("/redirect-url"), secure = false, "http://www.example.com/redirect-url") + } } "verifying presence of redirect param in the access token post request over https" in new WithApplication { - verifyPresenceOrAbsenceOfRedirectURL(Some("/redirect-url"), secure = true, "https://www.example.com/redirect-url") + override def running() = { + verifyPresenceOrAbsenceOfRedirectURL(Some("/redirect-url"), secure = true, "https://www.example.com/redirect-url") + } } "verifying absence of redirect param in the access token post request" in new WithApplication { - verifyPresenceOrAbsenceOfRedirectURL(None, secure = false, "http://www.example.com/request-path/redirect-url") + override def running() = { + verifyPresenceOrAbsenceOfRedirectURL(None, secure = false, "http://www.example.com/request-path/redirect-url") + } } "verifying absence of redirect param in the access token post request over https" in new WithApplication { - verifyPresenceOrAbsenceOfRedirectURL(None, secure = true, "https://www.example.com/redirect-url") + override def running() = { + verifyPresenceOrAbsenceOfRedirectURL(None, secure = true, "https://www.example.com/redirect-url") + } } def verifyRelativeRedirectResolution(redirectURL: String, secure: Boolean, resolvedRedirectURL: String) = { @@ -152,26 +176,26 @@ abstract class OAuth2ProviderSpec extends SocialStateProviderSpec[OAuth2Info, So implicit val req = FakeRequest[AnyContent]( method = GET, uri = "/request-path/something", - headers = FakeHeaders(Seq(HeaderNames.HOST -> "www.example.com")), + headers = FakeHeaders(Seq((HeaderNames.HOST, "www.example.com"))), body = AnyContentAsEmpty, secure = secure) val sessionKey = "session-key" val sessionValue = "session-value" - c.oAuthSettings.redirectURL returns Some(redirectURL) + when(c.oAuthSettings.redirectURL).thenReturn(Some(redirectURL)) - c.stateProvider.serialize(c.state) returns sessionValue - c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.state(any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.publish(any, any)(any) answers { (a, _) => - val result = a.asInstanceOf[Array[Any]](0).asInstanceOf[Result] + when(c.stateProvider.serialize(c.state)).thenReturn(sessionValue) + when(c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.publish(any, any)(any)).thenAnswer { m => + val result = m.getArgument(0).asInstanceOf[Result] result.withSession(sessionKey -> c.stateProvider.serialize(c.state)) } result(c.provider.authenticate()) { result => - redirectLocation(result) must beSome.which { url => + redirectLocation(result) must beSome[String].which { url => url must contain(s"$RedirectURI=${encode(resolvedRedirectURL, "UTF-8")}") } } @@ -185,20 +209,20 @@ abstract class OAuth2ProviderSpec extends SocialStateProviderSpec[OAuth2Info, So implicit val req = FakeRequest[AnyContent]( method = GET, uri = "/request-path/something", - headers = FakeHeaders(Seq(HeaderNames.HOST -> "www.example.com")), + headers = FakeHeaders(Seq((HeaderNames.HOST, "www.example.com"))), body = AnyContentAsEmpty, secure = secure) val sessionKey = "session-key" val sessionValue = "session-value" - c.oAuthSettings.redirectURL returns redirectURL + when(c.oAuthSettings.redirectURL).thenReturn(redirectURL) - c.stateProvider.serialize(c.state) returns sessionValue - c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.state(any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.publish(any, any)(any) answers { (a, _) => - val result = a.asInstanceOf[Array[Any]](0).asInstanceOf[Result] + when(c.stateProvider.serialize(c.state)).thenReturn(sessionValue) + when(c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.publish(any, any)(any)).thenAnswer { m => + val result = m.getArgument(0).asInstanceOf[Result] result.withSession(sessionKey -> c.stateProvider.serialize(c.state)) } @@ -206,13 +230,13 @@ abstract class OAuth2ProviderSpec extends SocialStateProviderSpec[OAuth2Info, So redirectURL match { case Some(_) => result(c.provider.authenticate()) { result => - redirectLocation(result) must beSome.which { url => + redirectLocation(result) must beSome[String].which { url => url must contain(s"$RedirectURI=${encode(resolvedRedirectURL, "UTF-8")}") } } case None => result(c.provider.authenticate()) { result => - redirectLocation(result) must beSome.which { url => + redirectLocation(result) must beSome[String].which { url => url must not contain s"$RedirectURI=" } } @@ -221,76 +245,82 @@ abstract class OAuth2ProviderSpec extends SocialStateProviderSpec[OAuth2Info, So } "not send state param if state is empty" in new WithApplication { - c.oAuthSettings.authorizationURL match { - case None => skipped("authorizationURL is not defined, so this step isn't needed for provider: " + c.provider.getClass) - case Some(_) => - implicit val req = FakeRequest(GET, "/") - - c.stateProvider.serialize(c.state) returns "" - c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.state(any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.publish(any, any)(any) answers { (a, _) => - a.asInstanceOf[Array[Any]](0).asInstanceOf[Result] - } + override def running() = { + c.oAuthSettings.authorizationURL match { + case None => skipped("authorizationURL is not defined, so this step isn't needed for provider: " + c.provider.getClass) + case Some(_) => + implicit val req = FakeRequest(GET, "/") + + when(c.stateProvider.serialize(c.state)).thenReturn("") + when(c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.publish(any, any)(any)).thenAnswer { m => + m.getArgument(0).asInstanceOf[Result] + } - result(c.provider.authenticate())(result => - redirectLocation(result) must beSome.which(_ must not contain State)) + result(c.provider.authenticate())(result => + redirectLocation(result) must beSome[String].which(_ must not contain State)) + } } } "submit the proper params to the access token post request" in new WithApplication { - val wsRequest = mock[MockWSRequest] - val redirectParam = c.oAuthSettings.redirectURL match { - case Some(rUri) => - List((RedirectURI, rUri)) - case None => Nil - } - val params = Map( - ClientID -> Seq(c.oAuthSettings.clientID), - ClientSecret -> Seq(c.oAuthSettings.clientSecret), - GrantType -> Seq(AuthorizationCode), - Code -> Seq("my.code")) ++ c.oAuthSettings.accessTokenParams.transformValues(Seq(_)) ++ redirectParam.toMap.transformValues(Seq(_)) - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsRequest.withHttpHeaders(any) returns wsRequest - - // We must use this neat trick here because it isn't possible to check the post call with a verification, - // because of the implicit params needed for the post call. On the other hand we can test it in the abstract - // spec, because we throw an exception in both cases which stops the test once the post method was called. - // This protects as for an NPE because of the not mocked dependencies. The other solution would be to execute - // this test in every provider with the full mocked dependencies. - wsRequest.post[Map[String, Seq[String]]](any)(any) answers { (a, _) => - if (a.asInstanceOf[Array[Any]](0).asInstanceOf[Map[String, Seq[String]]].equals(params)) { - throw new RuntimeException("success") - } else { - throw new RuntimeException("failure") + override def running() = { + val wsRequest = mock[MockWSRequest] + val redirectParam = c.oAuthSettings.redirectURL match { + case Some(rUri) => + List((RedirectURI, rUri)) + case None => Nil } - } - c.httpLayer.url(c.oAuthSettings.accessTokenURL) returns wsRequest - c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.state(any[ExecutionContext]) returns Future.successful(c.state) + val params = Map( + ClientID -> Seq(c.oAuthSettings.clientID), + ClientSecret -> Seq(c.oAuthSettings.clientSecret), + GrantType -> Seq(AuthorizationCode), + Code -> Seq("my.code")) ++ c.oAuthSettings.accessTokenParams.transformValues(Seq(_)) ++ redirectParam.toMap.transformValues(Seq(_)) + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + + // We must use this neat trick here because it isn't possible to check the post call with a verification, + // because of the implicit params needed for the post call. On the other hand we can test it in the abstract + // spec, because we throw an exception in both cases which stops the test once the post method was called. + // This protects as for an NPE because of the not mocked dependencies. The other solution would be to execute + // this test in every provider with the full mocked dependencies. + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenAnswer { m => + if (m.getArgument(0).asInstanceOf[Map[String, Seq[String]]].equals(params)) { + throw new RuntimeException("success") + } else { + throw new RuntimeException("failure") + } + } + when(c.httpLayer.url(c.oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(c.state)) - failed[RuntimeException](c.provider.authenticate()) { - case e => e.getMessage must startWith("success") + failed[RuntimeException](c.provider.authenticate()) { + case e => e.getMessage must startWith("success") + } } } "fail with UnexpectedResponseException if Json cannot be parsed from response" in new WithApplication { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - - wsResponse.status returns 200 - wsResponse.json throws new RuntimeException("Unexpected character ('<' (code 60))") - wsResponse.body returns "" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - c.httpLayer.url(c.oAuthSettings.accessTokenURL) returns wsRequest - c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(c.state) - c.stateProvider.state(any[ExecutionContext]) returns Future.successful(c.state) - - failed[UnexpectedResponseException](c.provider.authenticate()) { - case e => e.getMessage must startWith( - JsonParseError.format(c.provider.id, "", "java.lang.RuntimeException: Unexpected character ('<' (code 60))")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenThrow(new RuntimeException("Unexpected character ('<' (code 60))")) + when(wsResponse.body).thenReturn("") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(c.httpLayer.url(c.oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(c.stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(c.state)) + when(c.stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(c.state)) + + failed[UnexpectedResponseException](c.provider.authenticate()) { + case e => e.getMessage must startWith( + JsonParseError.format(c.provider.id, "", "java.lang.RuntimeException: Unexpected character ('<' (code 60))")) + } } } } @@ -313,7 +343,7 @@ abstract class OAuth2ProviderSpec extends SocialStateProviderSpec[OAuth2Info, So /** * Context for the OAuth2ProviderSpec. */ -trait OAuth2ProviderSpecContext extends Scope with Mockito with ThrownExpectations { +trait OAuth2ProviderSpecContext extends Scope with ThrownExpectations { abstract class TestState extends SocialState(Set.empty) abstract class TestStateProvider extends SocialStateHandler { @@ -326,8 +356,8 @@ trait OAuth2ProviderSpecContext extends Scope with Mockito with ThrownExpectatio * The HTTP layer mock. */ lazy val httpLayer = { - val m = mock[MockHTTPLayer].smart - m.executionContext returns global + val m = mockSmart[MockHTTPLayer] + when(m.executionContext).thenReturn(global) m } @@ -343,7 +373,7 @@ trait OAuth2ProviderSpecContext extends Scope with Mockito with ThrownExpectatio /** * The OAuth2 state. */ - lazy val state = mock[TestState].smart + lazy val state = mockSmart[TestState] /** * A user state item. @@ -353,7 +383,7 @@ trait OAuth2ProviderSpecContext extends Scope with Mockito with ThrownExpectatio /** * The OAuth2 state provider. */ - lazy val stateProvider = mock[TestStateProvider].smart + lazy val stateProvider = mockSmart[TestStateProvider] /** * The stateful auth info. diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OpenIDProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OpenIDProviderSpec.scala index 626d32e6..f31a7430 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OpenIDProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/OpenIDProviderSpec.scala @@ -19,12 +19,15 @@ import io.github.honeycombcheesecake.play.silhouette.api.util.HTTPLayer import io.github.honeycombcheesecake.play.silhouette.impl.exceptions.UnexpectedResponseException import io.github.honeycombcheesecake.play.silhouette.impl.providers.OpenIDProvider._ import org.specs2.matcher.ThrownExpectations -import org.specs2.mock.Mockito import org.specs2.specification.Scope +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito._ +import org.mockito.ArgumentCaptor import play.api.mvc.{ AnyContent, AnyContentAsEmpty } import play.api.test.{ FakeHeaders, FakeRequest, WithApplication } import play.mvc.Http.HeaderNames import test.SocialProviderSpec +import test.Helper.mock import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -40,76 +43,94 @@ abstract class OpenIDProviderSpec extends SocialProviderSpec[OpenIDInfo] { "The authenticate method" should { val c = context "fail with an UnexpectedResponseException if redirect URL couldn't be retrieved" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - c.openIDService.redirectURL(any(), any())(any()) returns Future.failed(new Exception("")) + when(c.openIDService.redirectURL(any(), any())(any())).thenReturn(Future.failed(new Exception(""))) - failed[UnexpectedResponseException](c.provider.authenticate()) { - case e => e.getMessage must startWith(ErrorRedirectURL.format(c.provider.id, "")) + failed[UnexpectedResponseException](c.provider.authenticate()) { + case e => e.getMessage must startWith(ErrorRedirectURL.format(c.provider.id, "")) + } } } "redirect to provider by using the provider URL" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - c.openIDService.redirectURL(any(), any())(any()) answers { _: Any => Future.successful(c.openIDSettings.providerURL) } - - result(c.provider.authenticate()) { result => - status(result) must equalTo(SEE_OTHER) - redirectLocation(result) must beSome.which(_ == c.openIDSettings.providerURL) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + when(c.openIDService.redirectURL(any(), any())(any())).thenAnswer(_ => Future.successful(c.openIDSettings.providerURL)) + + result(c.provider.authenticate()) { result => + status(result) must equalTo(SEE_OTHER) + redirectLocation(result) must beSome[String].which(_ == c.openIDSettings.providerURL) + } } } "redirect to provider by using a openID" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?openID=my.open.id") - c.openIDService.redirectURL(any(), any())(any()) answers { _: Any => Future.successful(c.openIDSettings.providerURL) } - - result(c.provider.authenticate()) { result => - status(result) must equalTo(SEE_OTHER) - redirectLocation(result) must beSome.which(_ == c.openIDSettings.providerURL) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?openID=my.open.id") + when(c.openIDService.redirectURL(any(), any())(any())).thenAnswer(_ => Future.successful(c.openIDSettings.providerURL)) + + result(c.provider.authenticate()) { result => + status(result) must equalTo(SEE_OTHER) + redirectLocation(result) must beSome[String].which(_ == c.openIDSettings.providerURL) + } } } "resolves relative callbackURLs before starting the flow" in new WithApplication { - verifyRelativeCallbackURLResolution("/callback-url", secure = false, "http://www.example.com/callback-url") + override def running() = { + verifyRelativeCallbackURLResolution("/callback-url", secure = false, "http://www.example.com/callback-url") + } } "resolves path relative callbackURLs before starting the flow" in new WithApplication { - verifyRelativeCallbackURLResolution("callback-url", secure = false, "http://www.example.com/request-path/callback-url") + override def running() = { + verifyRelativeCallbackURLResolution("callback-url", secure = false, "http://www.example.com/request-path/callback-url") + } } "resolves relative callbackURLs before starting the flow over https" in new WithApplication { - verifyRelativeCallbackURLResolution("/callback-url", secure = true, "https://www.example.com/callback-url") + override def running() = { + verifyRelativeCallbackURLResolution("/callback-url", secure = true, "https://www.example.com/callback-url") + } } def verifyRelativeCallbackURLResolution(callbackURL: String, secure: Boolean, resolvedCallbackURL: String) = { implicit val req = FakeRequest[AnyContent]( method = GET, uri = "/request-path/something", - headers = FakeHeaders(Seq(HeaderNames.HOST -> "www.example.com")), + headers = FakeHeaders(Seq((HeaderNames.HOST, "www.example.com"))), body = AnyContentAsEmpty, secure = secure) - c.openIDSettings.callbackURL returns callbackURL - c.openIDService.redirectURL(any(), any())(any()) answers { _: Any => Future.successful(c.openIDSettings.providerURL) } + when(c.openIDSettings.callbackURL).thenReturn(callbackURL) + when(c.openIDService.redirectURL(any(), any())(any())).thenAnswer(_ => Future.successful(c.openIDSettings.providerURL)) await(c.provider.authenticate()) - there was one(c.openIDService).redirectURL(any(), ===(resolvedCallbackURL))(any()) + val argument = ArgumentCaptor.forClass(classOf[String]) + verify(c.openIDService).redirectURL(any(), argument.capture())(any()) + assert(argument.getValue() === resolvedCallbackURL) } "fail with an UnexpectedResponseException if auth info cannot be retrieved" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Mode + "=id_res") - c.openIDService.verifiedID(any(), any()) returns Future.failed(new Exception("")) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Mode + "=id_res") + when(c.openIDService.verifiedID(any(), any())).thenReturn(Future.failed(new Exception(""))) - failed[UnexpectedResponseException](c.provider.authenticate()) { - case e => e.getMessage must startWith(ErrorVerification.format(c.provider.id, "")) + failed[UnexpectedResponseException](c.provider.authenticate()) { + case e => e.getMessage must startWith(ErrorVerification.format(c.provider.id, "")) + } } } "return the auth info" in new WithApplication { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Mode + "=id_res") - c.openIDService.verifiedID(any(), any()) answers { _: Any => Future.successful(c.openIDInfo) } + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Mode + "=id_res") + when(c.openIDService.verifiedID(any(), any())).thenAnswer(_ => Future.successful(c.openIDInfo)) - authInfo(c.provider.authenticate())(_ must be equalTo c.openIDInfo) + authInfo(c.provider.authenticate())(_ must be equalTo c.openIDInfo) + } } } @@ -131,14 +152,14 @@ abstract class OpenIDProviderSpec extends SocialProviderSpec[OpenIDInfo] { /** * Context for the OpenIDProviderSpec. */ -trait OpenIDProviderSpecContext extends Scope with Mockito with ThrownExpectations { +trait OpenIDProviderSpecContext extends Scope with ThrownExpectations { /** * The HTTP layer mock. */ lazy val httpLayer = { val m = mock[HTTPLayer] - m.executionContext returns global + when(m.executionContext).thenReturn(global) m } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/PasswordProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/PasswordProviderSpec.scala index 07bd244f..6f4fe2d5 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/PasswordProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/PasswordProviderSpec.scala @@ -17,14 +17,16 @@ package io.github.honeycombcheesecake.play.silhouette.impl.providers import io.github.honeycombcheesecake.play.silhouette.api.repositories.AuthInfoRepository import io.github.honeycombcheesecake.play.silhouette.api.util._ -import org.specs2.mock.Mockito import org.specs2.specification.Scope +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mock import play.api.test.PlaySpecification /** * Abstract test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.providers.PasswordProvider]] based class. */ -trait PasswordProviderSpec extends PlaySpecification with Mockito { +trait PasswordProviderSpec extends PlaySpecification { /** * The context. @@ -59,11 +61,11 @@ trait PasswordProviderSpec extends PlaySpecification with Mockito { */ private def hasher(id: String) = { val h = mock[PasswordHasher] - h.id returns id - h.isSuitable(any()) answers { p: Any => - p.asInstanceOf[PasswordInfo].hasher == h.id + when(h.id).thenReturn(id) + when(h.isSuitable(any())).thenAnswer { p => + p.getArgument(0).asInstanceOf[PasswordInfo].hasher == h.id } - h.isDeprecated(any()) returns Some(false) + when(h.isDeprecated(any())).thenReturn(Some(false)) h } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/SocialProviderRegistrySpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/SocialProviderRegistrySpec.scala index 607d665f..7cebdb84 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/SocialProviderRegistrySpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/SocialProviderRegistrySpec.scala @@ -18,14 +18,15 @@ package io.github.honeycombcheesecake.play.silhouette.impl.providers import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.TwitterProvider import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.{ GoogleProvider, FacebookProvider } import io.github.honeycombcheesecake.play.silhouette.impl.providers.openid.YahooProvider -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.test.PlaySpecification +import org.mockito.Mockito._ +import test.Helper.mock /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialProviderRegistry]] class. */ -class SocialProviderRegistrySpec extends PlaySpecification with Mockito { +class SocialProviderRegistrySpec extends PlaySpecification { "The `get` method" should { "return a provider by its type" in new Context { @@ -39,7 +40,7 @@ class SocialProviderRegistrySpec extends PlaySpecification with Mockito { "return a provider by its ID as SocialProvider" in new Context { val provider = registry.get[SocialProvider](GoogleProvider.ID) - provider must beSome.like { + provider must beSome[SocialProvider].like { case value => value.id must be equalTo providers(1).id value must beAnInstanceOf[SocialProvider] @@ -49,7 +50,7 @@ class SocialProviderRegistrySpec extends PlaySpecification with Mockito { "return a provider by its ID as OAuth2Provider" in new Context { val provider = registry.get[OAuth2Provider](GoogleProvider.ID) - provider must beSome.like { + provider must beSome[OAuth2Provider].like { case value => value.id must be equalTo providers(1).id value must beAnInstanceOf[OAuth2Provider] @@ -79,11 +80,11 @@ class SocialProviderRegistrySpec extends PlaySpecification with Mockito { */ val providers = { val facebook = mock[FacebookProvider] - facebook.id returns FacebookProvider.ID + when(facebook.id).thenReturn(FacebookProvider.ID) val google = mock[GoogleProvider] - google.id returns GoogleProvider.ID + when(google.id).thenReturn(GoogleProvider.ID) val twitter = mock[TwitterProvider] - twitter.id returns TwitterProvider.ID + when(twitter.id).thenReturn(TwitterProvider.ID) Seq( facebook, diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/custom/FacebookProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/custom/FacebookProviderSpec.scala index 1a858d9d..72892d38 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/custom/FacebookProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/custom/FacebookProviderSpec.scala @@ -27,6 +27,9 @@ import play.api.libs.json.{ JsValue, Json } import play.api.mvc.AnyContentAsEmpty import play.api.test.{ FakeRequest, WithApplication } import test.Helper +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ ExecutionContext, Future } @@ -38,113 +41,127 @@ class FacebookProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: CustomFacebookProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) + override def running() = { + val wsRequest = mock(classOf[MockWSRequest]) + val wsResponse = mock(classOf[MockWSRequest#Response]) + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) + override def running() = { + val wsRequest = mock(classOf[MockWSRequest]) + val wsResponse = mock(classOf[MockWSRequest#Response]) + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) + override def running() = { + val wsRequest = mock(classOf[MockWSRequest]) + val wsResponse = mock(classOf[MockWSRequest#Response]) + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 400 - wsResponse.json returns Helper.loadJson("providers/custom/facebook.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest + override def running() = { + val wsRequest = mock(classOf[MockWSRequest]) + val wsResponse = mock(classOf[MockWSRequest#Response]) + when(wsResponse.status).thenReturn(400) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/custom/facebook.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - "An active access token must be used to query information about the current user.", - "OAuthException", - 2500)) + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + "An active access token must be used to query information about the current user.", + "OAuthException", + 2500)) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest + override def running() = { + val wsRequest = mock(classOf[MockWSRequest]) + val wsResponse = mock(classOf[MockWSRequest#Response]) + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/custom/facebook.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest + override def running() = { + val wsRequest = mock(classOf[MockWSRequest]) + val wsResponse = mock(classOf[MockWSRequest#Response]) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/custom/facebook.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CustomSocialProfile( - loginInfo = LoginInfo(provider.id, "134405962728980"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://fbcdn-sphotos-g-a.akamaihd.net/hphotos-ak-ash2/t1/36245_155530314499277_2350717_n.jpg?lvh=1"), - gender = Some("male")) + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CustomSocialProfile( + loginInfo = LoginInfo(provider.id, "134405962728980"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://fbcdn-sphotos-g-a.akamaihd.net/hphotos-ak-ash2/t1/36245_155530314499277_2350717_n.jpg?lvh=1"), + gender = Some("male")) + } } } } @@ -182,7 +199,7 @@ class FacebookProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new CustomFacebookProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: CustomFacebookProvider = new CustomFacebookProvider(httpLayer, stateProvider, oAuthSettings) } /** diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/LinkedInProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/LinkedInProviderSpec.scala index 583f5169..6f1a0a9a 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/LinkedInProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/LinkedInProviderSpec.scala @@ -22,7 +22,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialProfil import io.github.honeycombcheesecake.play.silhouette.impl.providers._ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.LinkedInProvider._ import play.api.test.WithApplication +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any import test.Helper +import test.Helper.mock import scala.concurrent.Future @@ -33,80 +36,90 @@ class LinkedInProviderSpec extends OAuth1ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val overrideSettingsFunction: OAuth1Settings => OAuth1Settings = { s => - s.copy("new-request-token-url") + override def running() = { + val overrideSettingsFunction: OAuth1Settings => OAuth1Settings = { s => + s.copy("new-request-token-url") + } + val s: LinkedInProvider = provider.withSettings(overrideSettingsFunction) + + s.settings.requestTokenURL must be equalTo "new-request-token-url" + verify(oAuthService).withSettings(overrideSettingsFunction) } - val s = provider.withSettings(overrideSettingsFunction) - - s.settings.requestTokenURL must be equalTo "new-request-token-url" - there was one(oAuthService).withSettings(overrideSettingsFunction) } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/linkedin.error.json") - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 0, - Some("Unknown authentication scheme"), - Some("LY860UAC5U"), - Some(401), - Some(1390421660154L))) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/linkedin.error.json")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 0, + Some("Unknown authentication scheme"), + Some("LY860UAC5U"), + Some(401), + Some(1390421660154L))) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json throws new RuntimeException("") - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/linkedin.success.json") - httpLayer.url(url) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo)) - - there was one(httpLayer).url(url) + override def running() = { + val url = "https://custom.api.url" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/linkedin.success.json")) + when(httpLayer.url(url)).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo)) + + verify(httpLayer).url(url) + } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/linkedin.success.json") - httpLayer.url(API) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo)) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "NhZXBl_O6f"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("http://media.linkedin.com/mpr/mprx/0_fsPnURNRhLhk_Ue2fjKLUZkB2FL6TOe2S4bdUZz61GA9Ysxu_y_sz4THGW5JGJWhaMleN0F61-Dg")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/linkedin.success.json")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo)) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "NhZXBl_O6f"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("http://media.linkedin.com/mpr/mprx/0_fsPnURNRhLhk_Ue2fjKLUZkB2FL6TOe2S4bdUZz61GA9Ysxu_y_sz4THGW5JGJWhaMleN0F61-Dg")) + } } } } @@ -137,6 +150,6 @@ class LinkedInProviderSpec extends OAuth1ProviderSpec { /** * The provider to test. */ - lazy val provider = new LinkedInProvider(httpLayer, oAuthService, oAuthTokenSecretProvider, oAuthSettings) + lazy val provider: LinkedInProvider = new LinkedInProvider(httpLayer, oAuthService, oAuthTokenSecretProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/TwitterProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/TwitterProviderSpec.scala index a7dcf7fc..7fef246b 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/TwitterProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/TwitterProviderSpec.scala @@ -22,7 +22,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialProfil import io.github.honeycombcheesecake.play.silhouette.impl.providers._ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.TwitterProvider._ import play.api.test.WithApplication +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any import test.Helper +import test.Helper.mock import scala.concurrent.Future @@ -33,91 +36,103 @@ class TwitterProviderSpec extends OAuth1ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val overrideSettingsFunction: OAuth1Settings => OAuth1Settings = { s => - s.copy("new-request-token-url") + override def running() = { + val overrideSettingsFunction: OAuth1Settings => OAuth1Settings = { s => + s.copy("new-request-token-url") + } + val s: TwitterProvider = provider.withSettings(overrideSettingsFunction) + + s.settings.requestTokenURL must be equalTo "new-request-token-url" + verify(oAuthService).withSettings(overrideSettingsFunction) } - val s = provider.withSettings(overrideSettingsFunction) - - s.settings.requestTokenURL must be equalTo "new-request-token-url" - there was one(oAuthService).withSettings(overrideSettingsFunction) } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/twitter.error.json") - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 215, - Some("Bad Authentication data"))) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/twitter.error.json")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 215, + Some("Bad Authentication data"))) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json throws new RuntimeException("") - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/twitter.with.email.json") - httpLayer.url(url) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo)) - - there was one(httpLayer).url(url) + override def running() = { + val url = "https://custom.api.url" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/twitter.with.email.json")) + when(httpLayer.url(url)).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo)) + + verify(httpLayer).url(url) + } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/twitter.success.json") - httpLayer.url(API) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo)) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "6253282"), - fullName = Some("Apollonia Vanova"), - avatarURL = Some("https://pbs.twimg.com/profile_images/1209905677/appolonia_.jpg")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/twitter.success.json")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo)) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "6253282"), + fullName = Some("Apollonia Vanova"), + avatarURL = Some("https://pbs.twimg.com/profile_images/1209905677/appolonia_.jpg")) + } } } "return the social profile with email" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/twitter.with.email.json") - httpLayer.url(API) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo)) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "6253282"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://pbs.twimg.com/profile_images/1209905677/appolonia_.jpg")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/twitter.with.email.json")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo)) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "6253282"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://pbs.twimg.com/profile_images/1209905677/appolonia_.jpg")) + } } } } @@ -137,7 +152,7 @@ class TwitterProviderSpec extends OAuth1ProviderSpec { /** * The OAuth1 settings. */ - override lazy val oAuthSettings = spy(OAuth1Settings( + override lazy val oAuthSettings = org.mockito.Mockito.spy(OAuth1Settings( requestTokenURL = "https://twitter.com/oauth/request_token", accessTokenURL = "https://twitter.com/oauth/access_token", authorizationURL = "https://twitter.com/oauth/authenticate", @@ -148,6 +163,6 @@ class TwitterProviderSpec extends OAuth1ProviderSpec { /** * The provider to test. */ - lazy val provider = new TwitterProvider(httpLayer, oAuthService, oAuthTokenSecretProvider, oAuthSettings) + lazy val provider: TwitterProvider = new TwitterProvider(httpLayer, oAuthService, oAuthTokenSecretProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/XingProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/XingProviderSpec.scala index bfb7ce56..b82c6430 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/XingProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/XingProviderSpec.scala @@ -22,7 +22,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialProfil import io.github.honeycombcheesecake.play.silhouette.impl.providers._ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.XingProvider._ import play.api.test.WithApplication +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any import test.Helper +import test.Helper.mock import scala.concurrent.Future @@ -33,77 +36,87 @@ class XingProviderSpec extends OAuth1ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val overrideSettingsFunction: OAuth1Settings => OAuth1Settings = { s => - s.copy("new-request-token-url") + override def running() = { + val overrideSettingsFunction: OAuth1Settings => OAuth1Settings = { s => + s.copy("new-request-token-url") + } + val s: XingProvider = provider.withSettings(overrideSettingsFunction) + + s.settings.requestTokenURL must be equalTo "new-request-token-url" + verify(oAuthService).withSettings(overrideSettingsFunction) } - val s = provider.withSettings(overrideSettingsFunction) - - s.settings.requestTokenURL must be equalTo "new-request-token-url" - there was one(oAuthService).withSettings(overrideSettingsFunction) } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/xing.error.json") - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - "INVALID_PARAMETERS", - "Invalid parameters (Limit must be a non-negative number.)")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/xing.error.json")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + "INVALID_PARAMETERS", + "Invalid parameters (Limit must be a non-negative number.)")) + } } } "throw ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json throws new RuntimeException("") - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo)) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/xing.success.json") - httpLayer.url(url) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo)) - - there was one(httpLayer).url(url) + override def running() = { + val url = "https://custom.api.url" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/xing.success.json")) + when(httpLayer.url(url)).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo)) + + verify(httpLayer).url(url) + } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsRequest.sign(any) returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - wsResponse.json returns Helper.loadJson("providers/oauth1/xing.success.json") - httpLayer.url(API) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo)) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "1235468792"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - avatarURL = Some("http://www.xing.com/img/users/e/3/d/f94ef165a.123456,1.140x185.jpg"), - email = Some("apollonia.vanova@watchmen.com")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsRequest.sign(any)).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth1/xing.success.json")) + when(httpLayer.url(API)).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo)) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "1235468792"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + avatarURL = Some("http://www.xing.com/img/users/e/3/d/f94ef165a.123456,1.140x185.jpg"), + email = Some("apollonia.vanova@watchmen.com")) + } } } } @@ -134,6 +147,6 @@ class XingProviderSpec extends OAuth1ProviderSpec { /** * The provider to test. */ - lazy val provider = new XingProvider(httpLayer, oAuthService, oAuthTokenSecretProvider, oAuthSettings) + lazy val provider: XingProvider = new XingProvider(httpLayer, oAuthService, oAuthTokenSecretProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/secrets/CookieSecretSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/secrets/CookieSecretSpec.scala index a00dfe85..b41e9788 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/secrets/CookieSecretSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/secrets/CookieSecretSpec.scala @@ -23,12 +23,13 @@ import io.github.honeycombcheesecake.play.silhouette.impl.exceptions.OAuth1Token import io.github.honeycombcheesecake.play.silhouette.impl.providers.OAuth1Info import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.secrets.CookieSecret._ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.secrets.CookieSecretProvider._ -import org.specs2.control.NoLanguageFeatures import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.mvc.{ AnyContentAsEmpty, Cookie, Results } import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mockSmart import java.time.{ ZoneId, ZonedDateTime } import scala.concurrent.ExecutionContext.Implicits.global @@ -40,7 +41,7 @@ import scala.util.{ Failure, Success } /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.secrets.CookieSecret]] class. */ -class CookieSecretSpec extends PlaySpecification with Mockito with JsonMatchers with NoLanguageFeatures { +class CookieSecretSpec extends PlaySpecification with JsonMatchers { "The `isExpired` method of the secret" should { "return true if the secret is expired" in new Context { @@ -54,62 +55,76 @@ class CookieSecretSpec extends PlaySpecification with Mockito with JsonMatchers "The `serialize` method of the secret" should { "sign the cookie" in new WithApplication with Context { - serialize(secret, signer, crypter) + override def running() = { + serialize(secret, signer, crypter) - there was one(signer).sign(any()) + verify(signer).sign(any()) + } } "encrypt the cookie" in new WithApplication with Context { - serialize(secret, signer, crypter) + override def running() = { + serialize(secret, signer, crypter) - there was one(crypter).encrypt(any()) + verify(crypter).encrypt(any()) + } } } "The `unserialize` method of the secret" should { "throw an OAuth1TokenSecretException if a secret contains invalid json" in new WithApplication with Context { - val value = "invalid" - val msg = Pattern.quote(InvalidJson.format(value)) + override def running() = { + val value = "invalid" + val msg = Pattern.quote(InvalidJson.format(value)) - unserialize(crypter.encrypt(value), signer, crypter) must beFailedTry.withThrowable[OAuth1TokenSecretException](msg) + unserialize(crypter.encrypt(value), signer, crypter) must beFailedTry.withThrowable[OAuth1TokenSecretException](msg) + } } "throw an OAuth1TokenSecretException if a secret contains valid json but invalid secret" in new WithApplication with Context { - val value = "{ \"test\": \"test\" }" - val msg = "^" + Pattern.quote(InvalidSecretFormat.format("")) + ".*" + override def running() = { + val value = "{ \"test\": \"test\" }" + val msg = "^" + Pattern.quote(InvalidSecretFormat.format("")) + ".*" - unserialize(crypter.encrypt(value), signer, crypter) must beFailedTry.withThrowable[OAuth1TokenSecretException](msg) + unserialize(crypter.encrypt(value), signer, crypter) must beFailedTry.withThrowable[OAuth1TokenSecretException](msg) + } } "throw an OAuth1TokenSecretException if a secret is badly signed" in new WithApplication with Context { - signer.extract(any()) returns Failure(new Exception("Bad signature")) + override def running() = { + when(signer.extract(any())).thenReturn(Failure(new Exception("Bad signature"))) - val value = serialize(secret, signer, crypter) - val msg = Pattern.quote(InvalidCookieSignature) + val value = serialize(secret, signer, crypter) + val msg = Pattern.quote(InvalidCookieSignature) - unserialize(crypter.encrypt(value), signer, crypter) must beFailedTry.withThrowable[OAuth1TokenSecretException](msg) + unserialize(crypter.encrypt(value), signer, crypter) must beFailedTry.withThrowable[OAuth1TokenSecretException](msg) + } } } "The `serialize/unserialize` method of the secret" should { "serialize/unserialize a secret" in new WithApplication with Context { - val serialized = serialize(secret, signer, crypter) + override def running() = { + val serialized = serialize(secret, signer, crypter) - unserialize(serialized, signer, crypter) must beSuccessfulTry.withValue(secret) + unserialize(serialized, signer, crypter) must beSuccessfulTry.withValue(secret) + } } } "The `build` method of the provider" should { "return a new secret" in new WithApplication with Context { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - val dateTime = ZonedDateTime.of(2014, 8, 8, 0, 0, 0, 0, ZoneId.systemDefault) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() + val dateTime = ZonedDateTime.of(2014, 8, 8, 0, 0, 0, 0, ZoneId.systemDefault) - clock.now returns dateTime + when(clock.now).thenReturn(dateTime) - val s = await(provider.build(oAuthInfo)) + val s = await(provider.build(oAuthInfo)) - s.expirationDate must be equalTo dateTime.plusSeconds(settings.expirationTime.toSeconds.toInt) - s.value must be equalTo oAuthInfo.secret + s.expirationDate must be equalTo dateTime.plusSeconds(settings.expirationTime.toSeconds.toInt) + s.value must be equalTo oAuthInfo.secret + } } } @@ -123,60 +138,72 @@ class CookieSecretSpec extends PlaySpecification with Mockito with JsonMatchers } "throw an OAuth1TokenSecretException if secret is expired" in new WithApplication with Context { - val expiredSecret = secret.copy(expirationDate = ZonedDateTime.now.minusHours(1)) + override def running() = { + val expiredSecret = secret.copy(expirationDate = ZonedDateTime.now.minusHours(1)) - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, CookieSecret.serialize(expiredSecret, signer, crypter))) + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, CookieSecret.serialize(expiredSecret, signer, crypter))) - await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { - case e => e.getMessage must startWith(SecretIsExpired.format()) + await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { + case e => e.getMessage must startWith(SecretIsExpired.format()) + } } } "throw an OAuth1TokenSecretException if client secret contains invalid json" in new WithApplication with Context { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, crypter.encrypt("{"))) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, crypter.encrypt("{"))) - await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { - case e => e.getMessage must startWith(InvalidJson.format("{")) + await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { + case e => e.getMessage must startWith(InvalidJson.format("{")) + } } } "throw an OAuth1TokenSecretException if client secret contains valid json but invalid secret" in new WithApplication with Context { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, crypter.encrypt("{ \"test\": \"test\" }"))) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, crypter.encrypt("{ \"test\": \"test\" }"))) - await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { - case e => e.getMessage must startWith(InvalidSecretFormat.format("")) + await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { + case e => e.getMessage must startWith(InvalidSecretFormat.format("")) + } } } "throw an OAuth1TokenSecretException if client secret is badly signed" in new WithApplication with Context { - signer.extract(any()) returns Failure(new Exception("Bad signature")) + override def running() = { + when(signer.extract(any())).thenReturn(Failure(new Exception("Bad signature"))) - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, CookieSecret.serialize(secret, signer, crypter))) + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, CookieSecret.serialize(secret, signer, crypter))) - await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { - case e => e.getMessage must startWith(InvalidCookieSignature) + await(provider.retrieve) must throwA[OAuth1TokenSecretException].like { + case e => e.getMessage must startWith(InvalidCookieSignature) + } } } "return the secret if it's valid" in new WithApplication with Context { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, CookieSecret.serialize(secret, signer, crypter))) + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withCookies(Cookie(settings.cookieName, CookieSecret.serialize(secret, signer, crypter))) - await(provider.retrieve) must be equalTo secret + await(provider.retrieve) must be equalTo secret + } } } "The `publish` method of the provider" should { "add the secret to the cookie" in new WithApplication with Context { - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/") - val result = Future.successful(provider.publish(Results.Ok, secret)) - - cookies(result).get(settings.cookieName) should beSome[Cookie].which { c => - c.name must be equalTo settings.cookieName - unserialize(c.value, signer, crypter).get must be equalTo secret - c.maxAge must beSome(settings.expirationTime.toSeconds.toInt) - c.path must be equalTo settings.cookiePath - c.domain must be equalTo settings.cookieDomain - c.secure must be equalTo settings.secureCookie + override def running() = { + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/") + val result = Future.successful(provider.publish(Results.Ok, secret)) + + cookies(result).get(settings.cookieName) should beSome[Cookie].which { c => + c.name must be equalTo settings.cookieName + unserialize(c.value, signer, crypter).get must be equalTo secret + c.maxAge must beSome(settings.expirationTime.toSeconds.toInt) + c.path must be equalTo settings.cookiePath + c.domain must be equalTo settings.cookieDomain + c.secure must be equalTo settings.secureCookie + } } } } @@ -189,7 +216,7 @@ class CookieSecretSpec extends PlaySpecification with Mockito with JsonMatchers /** * The clock implementation. */ - lazy val clock: Clock = mock[Clock].smart + lazy val clock: Clock = mockSmart[Clock] /** * The settings. @@ -205,9 +232,9 @@ class CookieSecretSpec extends PlaySpecification with Mockito with JsonMatchers * none cookie values in a cookie. */ lazy val crypter = { - val c = mock[Crypter].smart - c.encrypt(any()) answers { p: Any => Base64.encode(p.asInstanceOf[String]) } - c.decrypt(any()) answers { p: Any => Base64.decode(p.asInstanceOf[String]) } + val c = mockSmart[Crypter] + when(c.encrypt(any())).thenAnswer(p => Base64.encode(p.getArgument(0).asInstanceOf[String])) + when(c.decrypt(any())).thenAnswer(p => Base64.decode(p.getArgument(0).asInstanceOf[String])) c } @@ -217,9 +244,9 @@ class CookieSecretSpec extends PlaySpecification with Mockito with JsonMatchers * The signer returns the same value as passed to the methods. This is enough for testing. */ lazy val signer = { - val c = mock[Signer].smart - c.sign(any()) answers { p: Any => p.asInstanceOf[String] } - c.extract(any()) answers { p: Any => Success(p.asInstanceOf[String]) } + val c = mockSmart[Signer] + when(c.sign(any())).thenAnswer(_.getArgument(0).asInstanceOf[String]) + when(c.extract(any())).thenAnswer(p => Success(p.getArgument(0).asInstanceOf[String])) c } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/services/PlayOAuth1ServiceSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/services/PlayOAuth1ServiceSpec.scala index 16233d04..4d655d9a 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/services/PlayOAuth1ServiceSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth1/services/PlayOAuth1ServiceSpec.scala @@ -16,27 +16,29 @@ package io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth1.services import io.github.honeycombcheesecake.play.silhouette.impl.providers.{ OAuth1Info, OAuth1Settings } -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.oauth.{ OAuth, RequestToken } import play.api.libs.ws.WSSignatureCalculator import play.api.test.{ PlaySpecification, WithApplication } import play.shaded.oauth.oauth.signpost.exception.{ OAuthException, OAuthMessageSignerException } +import org.mockito.Mockito._ import scala.concurrent.ExecutionContext.Implicits.global /** * Test case for the [[PlayOAuth1Service]] class. */ -class PlayOAuth1ServiceSpec extends PlaySpecification with Mockito { +class PlayOAuth1ServiceSpec extends PlaySpecification { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = service.withSettings { s => - s.copy("new-request-token-url") - } + override def running() = { + val s = service.withSettings { s => + s.copy("new-request-token-url") + } - s.settings.requestTokenURL must be equalTo "new-request-token-url" + s.settings.requestTokenURL must be equalTo "new-request-token-url" + } } } @@ -48,13 +50,13 @@ class PlayOAuth1ServiceSpec extends PlaySpecification with Mockito { "The `use10a` method" should { "return true if the safer 1.0a specification will be used" in new Context { - oauth.use10a returns true + when(oauth.use10a).thenReturn(true) service.use10a must beTrue } "return false if the unsafer 1.0 specification will be used" in new Context { - oauth.use10a returns false + when(oauth.use10a).thenReturn(false) service.use10a must beFalse } @@ -62,13 +64,13 @@ class PlayOAuth1ServiceSpec extends PlaySpecification with Mockito { "The `retrieveRequestToken` method" should { "throw exception if the token couldn't be retrieved" in new Context { - oauth.retrieveRequestToken(settings.callbackURL) returns Left(new OAuthMessageSignerException("")) + when(oauth.retrieveRequestToken(settings.callbackURL)).thenReturn(Left(new OAuthMessageSignerException(""))) await(service.retrieveRequestToken(settings.callbackURL)) must throwA[OAuthException] } "return request token" in new Context { - oauth.retrieveRequestToken(settings.callbackURL) returns Right(token) + when(oauth.retrieveRequestToken(settings.callbackURL)).thenReturn(Right(token)) await(service.retrieveRequestToken(settings.callbackURL)) must be equalTo info } @@ -76,13 +78,13 @@ class PlayOAuth1ServiceSpec extends PlaySpecification with Mockito { "The `retrieveAccessToken` method" should { "throw Exception if the token couldn't be retrieved" in new Context { - oauth.retrieveAccessToken(token, "") returns Left(new OAuthMessageSignerException("")) + when(oauth.retrieveAccessToken(token, "")).thenReturn(Left(new OAuthMessageSignerException(""))) await(service.retrieveAccessToken(info, "")) must throwA[OAuthException] } "return access token" in new Context { - oauth.retrieveAccessToken(token, "") returns Right(token) + when(oauth.retrieveAccessToken(token, "")).thenReturn(Right(token)) await(service.retrieveAccessToken(info, "")) must be equalTo info } @@ -90,7 +92,7 @@ class PlayOAuth1ServiceSpec extends PlaySpecification with Mockito { "The `redirectUrl` method" should { "return the redirect Url" in new Context { - oauth.redirectUrl("token") returns "http://redirect.url" + when(oauth.redirectUrl("token")).thenReturn("http://redirect.url") service.redirectUrl("token") must be equalTo "http://redirect.url" } @@ -98,7 +100,7 @@ class PlayOAuth1ServiceSpec extends PlaySpecification with Mockito { "The `sign` method" should { "return the signature calculator" in new Context { - oauth.info returns PlayOAuth1Service.serviceInfo(settings) + when(oauth.info).thenReturn(PlayOAuth1Service.serviceInfo(settings)) service.sign(info) must beAnInstanceOf[WSSignatureCalculator] } @@ -133,7 +135,7 @@ class PlayOAuth1ServiceSpec extends PlaySpecification with Mockito { /** * A mock of the Play Framework OAuth implementation. */ - lazy val oauth: OAuth = mock[OAuth] + lazy val oauth: OAuth = mock(classOf[OAuth]) /** * The service to test. diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/Auth0ProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/Auth0ProviderSpec.scala index 94b1fd7f..f3b2ceab 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/Auth0ProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/Auth0ProviderSpec.scala @@ -24,7 +24,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.Auth0 import play.api.mvc.AnyContentAsEmpty import play.api.libs.json.Json import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -35,133 +38,149 @@ class Auth0ProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: Auth0Provider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - - wsResponse.status returns 400 - wsRequest.get() returns Future.successful(wsResponse) - wsRequest.withHttpHeaders(("Authorization", s"Bearer ${oAuthInfoObject.accessToken}")) returns wsRequest - httpLayer.url(oAuthSettings.apiURL.get) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfoObject)) { - case e => e.getMessage must equalTo(GenericHttpStatusProfileError.format(provider.id, 400)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + + when(wsResponse.status).thenReturn(400) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsRequest.withHttpHeaders(("Authorization", s"Bearer ${oAuthInfoObject.accessToken}"))).thenReturn(wsRequest) + when(httpLayer.url(oAuthSettings.apiURL.get)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfoObject)) { + case e => e.getMessage must equalTo(GenericHttpStatusProfileError.format(provider.id, 400)) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - wsRequest.withHttpHeaders(("Authorization", s"Bearer ${oAuthInfoObject.accessToken}")) returns wsRequest - httpLayer.url(oAuthSettings.apiURL.get) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo("[Silhouette][auth0] error retrieving profile information") + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsRequest.withHttpHeaders(("Authorization", s"Bearer ${oAuthInfoObject.accessToken}"))).thenReturn(wsRequest) + when(httpLayer.url(oAuthSettings.apiURL.get)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo("[Silhouette][auth0] error retrieving profile information") + } } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - val userProfile = Helper.loadJson(Auth0UserProfileJson) - - wsResponse.status returns 200 - wsResponse.json returns userProfile - wsRequest.get() returns Future.successful(wsResponse) - wsRequest.withHttpHeaders(("Authorization", s"Bearer ${oAuthInfoObject.accessToken}")) returns wsRequest - httpLayer.url(oAuthSettings.apiURL.get) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, (userProfile \ "sub").as[String]), - fullName = (userProfile \ "name").asOpt[String], - email = (userProfile \ "email").asOpt[String], - avatarURL = (userProfile \ "picture").asOpt[String]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + val userProfile = Helper.loadJson("providers/custom/auth0.profile.json") + + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(userProfile) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(wsRequest.withHttpHeaders(("Authorization", s"Bearer ${oAuthInfoObject.accessToken}"))).thenReturn(wsRequest) + when(httpLayer.url(oAuthSettings.apiURL.get)).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, (userProfile \ "sub").as[String]), + fullName = (userProfile \ "name").asOpt[String], + email = (userProfile \ "email").asOpt[String], + avatarURL = (userProfile \ "picture").asOpt[String]) + } } } } @@ -178,13 +197,6 @@ class Auth0ProviderSpec extends OAuth2ProviderSpec { */ trait Context extends OAuth2ProviderSpecContext { - /** - * Paths to the Json fixtures. - */ - val Auth0ErrorJson = "providers/custom/auth0.error.json" - val Auth0SuccessJson = "providers/custom/auth0.success.json" - val Auth0UserProfileJson = "providers/custom/auth0.profile.json" - /** * The OAuth2 settings. */ @@ -200,7 +212,7 @@ class Auth0ProviderSpec extends OAuth2ProviderSpec { /** * The OAuth2 info returned by Auth0. */ - override lazy val oAuthInfo = Helper.loadJson(Auth0SuccessJson) + override lazy val oAuthInfo = Helper.loadJson("providers/custom/auth0.success.json") /** * The OAuth2 info deserialized as case class object @@ -215,6 +227,6 @@ class Auth0ProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new Auth0Provider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: Auth0Provider = new Auth0Provider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/DropboxProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/DropboxProviderSpec.scala index 5377e49f..d07a6500 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/DropboxProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/DropboxProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.Dropb import play.api.mvc.AnyContentAsEmpty import play.api.libs.json.Json import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,151 +39,169 @@ class DropboxProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: DropboxProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.json returns Helper.loadJson("providers/oauth2/dropbox.error.json") - wsResponse.status returns 400 - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - "Invalid OAuth request.", - 400)) + override def running() = { + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/dropbox.error.json")) + when(wsResponse.status).thenReturn(400) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + "Invalid OAuth request.", + 400)) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url" - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/dropbox.success.json") - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url) returns wsRequest - - await(provider.retrieveProfile(authInfo)) - - there was one(httpLayer).url(url) + override def running() = { + val url = "https://custom.api.url" + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/dropbox.success.json")) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url)).thenReturn(wsRequest) + + await(provider.retrieveProfile(authInfo)) + + verify(httpLayer).url(url) + } } "return the social profile" in new WithApplication with Context { - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/dropbox.success.json") - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(authInfo)) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "12345678"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova")) + override def running() = { + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/dropbox.success.json")) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(authInfo)) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "12345678"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova")) + } } } } @@ -223,6 +244,6 @@ class DropboxProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new DropboxProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: DropboxProvider = new DropboxProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FacebookProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FacebookProviderSpec.scala index f9ec2591..54ecb8fb 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FacebookProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FacebookProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.Faceb import play.api.mvc.AnyContentAsEmpty import play.api.libs.json.Json import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,146 +39,164 @@ class FacebookProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: FacebookProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 400 - wsResponse.json returns Helper.loadJson("providers/oauth2/facebook.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - "An active access token must be used to query information about the current user.", - "OAuthException", - 2500)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(400) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/facebook.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + "An active access token must be used to query information about the current user.", + "OAuthException", + 2500)) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url?access_token=%s" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/facebook.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url.format("my.access.token")) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) - - there was one(httpLayer).url(url.format("my.access.token")) + override def running() = { + val url = "https://custom.api.url?access_token=%s" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/facebook.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url.format("my.access.token"))).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) + + verify(httpLayer).url(url.format("my.access.token")) + } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/facebook.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "134405962728980"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://fbcdn-sphotos-g-a.akamaihd.net/hphotos-ak-ash2/t1/36245_155530314499277_2350717_n.jpg?lvh=1")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/facebook.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "134405962728980"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://fbcdn-sphotos-g-a.akamaihd.net/hphotos-ak-ash2/t1/36245_155530314499277_2350717_n.jpg?lvh=1")) + } } } } @@ -218,6 +239,6 @@ class FacebookProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new FacebookProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: FacebookProvider = new FacebookProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FoursquareProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FoursquareProviderSpec.scala index d2fe157a..676b7212 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FoursquareProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/FoursquareProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.Fours import play.api.mvc.AnyContentAsEmpty import play.api.libs.json.Json import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,205 +39,229 @@ class FoursquareProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: FoursquareProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 400 - wsResponse.json returns Helper.loadJson("providers/oauth2/foursquare.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token", DefaultAPIVersion)) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 400, - Some("param_error"), - Some("Must provide a valid user ID or 'self.'"))) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(400) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/foursquare.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token", DefaultAPIVersion))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 400, + Some("param_error"), + Some("Must provide a valid user ID or 'self.'"))) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token", DefaultAPIVersion)) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token", DefaultAPIVersion))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url?access_token=%s" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/foursquare.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url.format("my.access.token")) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) - - there was one(httpLayer).url(url.format("my.access.token")) + override def running() = { + val url = "https://custom.api.url?access_token=%s" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/foursquare.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url.format("my.access.token"))).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) + + verify(httpLayer).url(url.format("my.access.token")) + } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/foursquare.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token", DefaultAPIVersion)) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "13221052"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://irs0.4sqi.net/img/user/100x100/blank_girl.png")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/foursquare.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token", DefaultAPIVersion))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "13221052"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://irs0.4sqi.net/img/user/100x100/blank_girl.png")) + } } } "return the social profile if API is deprecated" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/foursquare.deprecated.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token", DefaultAPIVersion)) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "13221052"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://irs0.4sqi.net/img/user/100x100/blank_girl.png")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/foursquare.deprecated.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token", DefaultAPIVersion))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "13221052"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://irs0.4sqi.net/img/user/100x100/blank_girl.png")) + } } } "handle the custom API version property" in new WithApplication with Context { - val customProperties = Map(APIVersion -> "20120101") - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/foursquare.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token", "20120101")) returns wsRequest - - profile(provider.withSettings(_.copy(customProperties = customProperties)) - .retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "13221052"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://irs0.4sqi.net/img/user/100x100/blank_girl.png")) + override def running() = { + val customProperties = Map(APIVersion -> "20120101") + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/foursquare.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token", "20120101"))).thenReturn(wsRequest) + + profile(provider.withSettings(_.copy(customProperties = customProperties)) + .retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "13221052"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://irs0.4sqi.net/img/user/100x100/blank_girl.png")) + } } } "handle the custom avatar resolution property" in new WithApplication with Context { - val customProperties = Map(AvatarResolution -> "150x150") - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/foursquare.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token", DefaultAPIVersion)) returns wsRequest - - profile(provider.withSettings(_.copy(customProperties = customProperties)) - .retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "13221052"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://irs0.4sqi.net/img/user/150x150/blank_girl.png")) + override def running() = { + val customProperties = Map(AvatarResolution -> "150x150") + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/foursquare.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token", DefaultAPIVersion))).thenReturn(wsRequest) + + profile(provider.withSettings(_.copy(customProperties = customProperties)) + .retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "13221052"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://irs0.4sqi.net/img/user/150x150/blank_girl.png")) + } } } } @@ -276,6 +303,6 @@ class FoursquareProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new FoursquareProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: FoursquareProvider = new FoursquareProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitHubProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitHubProviderSpec.scala index 3da08694..69487856 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitHubProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitHubProviderSpec.scala @@ -26,7 +26,10 @@ import play.api.http.HeaderNames import play.api.mvc.AnyContentAsEmpty import play.api.libs.json.Json import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -37,151 +40,169 @@ class GitHubProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: GitHubProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(HeaderNames.ACCEPT -> "application/json") returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(HeaderNames.ACCEPT -> "application/json")).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(HeaderNames.ACCEPT -> "application/json") returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(HeaderNames.ACCEPT -> "application/json")).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 400 - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsResponse.json returns Helper.loadJson("providers/oauth2/github.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - "Bad credentials", - Some("http://developer.github.com/v3"))) + override def running() = { + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(400) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/github.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + "Bad credentials", + Some("http://developer.github.com/v3"))) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API)).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(authInfo)) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url" - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/github.success.json") - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url) returns wsRequest - - await(provider.retrieveProfile(authInfo)) - - there was one(httpLayer).url(url) + override def running() = { + val url = "https://custom.api.url" + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/github.success.json")) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url)).thenReturn(wsRequest) + + await(provider.retrieveProfile(authInfo)) + + verify(httpLayer).url(url) + } } "return the social profile" in new WithApplication with Context { - val authInfo = oAuthInfo.as[OAuth2Info] - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/github.success.json") - wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}") returns wsRequest - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API) returns wsRequest - - profile(provider.retrieveProfile(authInfo)) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "1"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://github.com/images/error/apollonia_vanova.gif")) + override def running() = { + val authInfo = oAuthInfo.as[OAuth2Info] + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/github.success.json")) + when(wsRequest.withHttpHeaders(AUTHORIZATION -> s"Bearer ${authInfo.accessToken}")).thenReturn(wsRequest) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API)).thenReturn(wsRequest) + + profile(provider.retrieveProfile(authInfo)) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "1"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://github.com/images/error/apollonia_vanova.gif")) + } } } } @@ -224,6 +245,6 @@ class GitHubProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new GitHubProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: GitHubProvider = new GitHubProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitLabProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitLabProviderSpec.scala index f6df2584..5923939a 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitLabProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GitLabProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.GitLa import play.api.libs.json.Json import play.api.mvc.AnyContentAsEmpty import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,142 +39,160 @@ class GitLabProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: GitLabProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 400 - wsResponse.json returns Helper.loadJson("providers/oauth2/gitlab.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - "Bad credentials")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(400) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/gitlab.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + "Bad credentials")) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url?access_token=%s" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/gitlab.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url.format("my.access.token")) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) - - there was one(httpLayer).url(url.format("my.access.token")) + override def running() = { + val url = "https://custom.api.url?access_token=%s" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/gitlab.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url.format("my.access.token"))).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) + + verify(httpLayer).url(url.format("my.access.token")) + } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/gitlab.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "1"), - fullName = Some("John Smith"), - email = Some("john@example.com"), - avatarURL = Some("http://gitlab.com/uploads/user/avatar/1/index.jpg")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/gitlab.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "1"), + fullName = Some("John Smith"), + email = Some("john@example.com"), + avatarURL = Some("http://gitlab.com/uploads/user/avatar/1/index.jpg")) + } } } } @@ -214,7 +235,7 @@ class GitLabProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new GitLabProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: GitLabProvider = new GitLabProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GoogleProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GoogleProviderSpec.scala index 5b5243bc..973b0b95 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GoogleProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/GoogleProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.Googl import play.api.libs.json.Json import play.api.mvc.AnyContentAsEmpty import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,218 +39,244 @@ class GoogleProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: GoogleProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 401 - wsResponse.json returns Helper.loadJson("providers/oauth2/google.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 401, - "Invalid Credentials")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(401) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/google.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 401, + "Invalid Credentials")) + } } } "fail with ProfileRetrievalException if API returns missing People API config" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 403 - wsResponse.json returns Helper.loadJson("providers/oauth2/google.error.api.missing.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - val apiErrMsg = "People API has not been used in project 1234567890 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=1234567890 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 403, - apiErrMsg)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(403) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/google.error.api.missing.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + val apiErrMsg = "People API has not been used in project 1234567890 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=1234567890 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 403, + apiErrMsg)) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url?access_token=%s" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/google.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url.format("my.access.token")) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) - - there was one(httpLayer).url(url.format("my.access.token")) + override def running() = { + val url = "https://custom.api.url?access_token=%s" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/google.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url.format("my.access.token"))).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) + + verify(httpLayer).url(url.format("my.access.token")) + } } "return the social profile with an email" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/google.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "109476598527568979481"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/google.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "109476598527568979481"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")) + } } } "return the social profile without an email" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/google.without.email.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "109476598527568979481"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = None, - avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/google.without.email.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "109476598527568979481"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = None, + avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")) + } } } "return the social profile with an avatar url" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/google.img.non-default.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "109476598527568979481"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/google.img.non-default.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "109476598527568979481"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")) + } } } "return the social profile without an avatar url" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/google.img.default.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "109476598527568979481"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = None) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/google.img.default.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "109476598527568979481"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = None) + } } } } @@ -290,6 +319,6 @@ class GoogleProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new GoogleProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: GoogleProvider = new GoogleProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/InstagramProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/InstagramProviderSpec.scala index 31efe11e..468810b1 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/InstagramProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/InstagramProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.Insta import play.api.libs.json.Json import play.api.mvc.AnyContentAsEmpty import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,143 +39,161 @@ class InstagramProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: InstagramProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 400 - wsResponse.json returns Helper.loadJson("providers/oauth2/instagram.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 400, - Some("OAuthAccessTokenException"), - Some("The access_token provided is invalid."))) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(400) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/instagram.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 400, + Some("OAuthAccessTokenException"), + Some("The access_token provided is invalid."))) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url?access_token=%s" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/instagram.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url.format("my.access.token")) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) - - there was one(httpLayer).url(url.format("my.access.token")) + override def running() = { + val url = "https://custom.api.url?access_token=%s" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/instagram.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url.format("my.access.token"))).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) + + verify(httpLayer).url(url.format("my.access.token")) + } } "return the social profile" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/instagram.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "1574083"), - fullName = Some("Apollonia Vanova"), - avatarURL = Some("http://distillery.s3.amazonaws.com/profiles/profile_1574083_75sq_1295469061.jpg")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/instagram.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "1574083"), + fullName = Some("Apollonia Vanova"), + avatarURL = Some("http://distillery.s3.amazonaws.com/profiles/profile_1574083_75sq_1295469061.jpg")) + } } } } @@ -215,6 +236,6 @@ class InstagramProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new InstagramProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: InstagramProvider = new InstagramProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/LinkedInProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/LinkedInProviderSpec.scala index 07273cb8..44d0b8c7 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/LinkedInProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/LinkedInProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.Linke import play.api.libs.json.Json import play.api.mvc.AnyContentAsEmpty import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,169 +39,187 @@ class LinkedInProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: LinkedInProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 401 - wsResponse.json returns Helper.loadJson("providers/oauth2/linkedin.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - mockEmailAndPhoto(httpLayer) - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 0, - Some("Unknown authentication scheme"), - Some("LY860UAC5U"), - Some(401), - Some(1390421660154L))) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(401) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/linkedin.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + mockEmailAndPhoto(httpLayer) + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 0, + Some("Unknown authentication scheme"), + Some("LY860UAC5U"), + Some(401), + Some(1390421660154L))) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - mockEmailAndPhoto(httpLayer) - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + mockEmailAndPhoto(httpLayer) + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url?access_token=%s" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/linkedin.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url.format("my.access.token")) returns wsRequest - mockEmailAndPhoto(httpLayer) - await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) - - there was one(httpLayer).url(url.format("my.access.token")) + override def running() = { + val url = "https://custom.api.url?access_token=%s" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/linkedin.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url.format("my.access.token"))).thenReturn(wsRequest) + mockEmailAndPhoto(httpLayer) + await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) + + verify(httpLayer).url(url.format("my.access.token")) + } } def mockEmailAndPhoto(httpLayer: MockHTTPLayer) = { // Email val wsRequestEmail = mock[MockWSRequest] val wsResponseEmail = mock[MockWSRequest#Response] - wsResponseEmail.status returns 200 - wsResponseEmail.json returns Helper.loadJson("providers/oauth2/linkedin.email.json") - wsRequestEmail.get() returns Future.successful(wsResponseEmail) - httpLayer.url(EMAIL.format("my.access.token")) returns wsRequestEmail + when(wsResponseEmail.status).thenReturn(200) + when(wsResponseEmail.json).thenReturn(Helper.loadJson("providers/oauth2/linkedin.email.json")) + when(wsRequestEmail.get()).thenReturn(Future.successful(wsResponseEmail)) + when(httpLayer.url(EMAIL.format("my.access.token"))).thenReturn(wsRequestEmail) // Photo val wsRequestPhoto = mock[MockWSRequest] val wsResponsePhoto = mock[MockWSRequest#Response] - wsResponsePhoto.status returns 200 - wsResponsePhoto.json returns Helper.loadJson("providers/oauth2/linkedin.photo.json") - wsRequestPhoto.get() returns Future.successful(wsResponsePhoto) - httpLayer.url(PHOTO.format("my.access.token")) returns wsRequestPhoto + when(wsResponsePhoto.status).thenReturn(200) + when(wsResponsePhoto.json).thenReturn(Helper.loadJson("providers/oauth2/linkedin.photo.json")) + when(wsRequestPhoto.get()).thenReturn(Future.successful(wsResponsePhoto)) + when(httpLayer.url(PHOTO.format("my.access.token"))).thenReturn(wsRequestPhoto) } "return the social profile" in new WithApplication with Context { - // Basic profile - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/linkedin.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - mockEmailAndPhoto(httpLayer) - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "NhZXBl_O6f"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://media.licdn.com/dms/image/C4E03AQFBprjocrF2iA/profile-displayphoto-shrink_100_100/0?e=1576108800&v=beta&t=Tn7mA43w8qmTuzjSdtuYQMi2kI5At9XOp8X--s5hpRU")) + override def running() = { + // Basic profile + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/linkedin.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + mockEmailAndPhoto(httpLayer) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "NhZXBl_O6f"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://media.licdn.com/dms/image/C4E03AQFBprjocrF2iA/profile-displayphoto-shrink_100_100/0?e=1576108800&v=beta&t=Tn7mA43w8qmTuzjSdtuYQMi2kI5At9XOp8X--s5hpRU")) + } } } } @@ -241,6 +262,6 @@ class LinkedInProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new LinkedInProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: LinkedInProvider = new LinkedInProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/VKProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/VKProviderSpec.scala index e0897735..8ebd238b 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/VKProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/oauth2/VKProviderSpec.scala @@ -25,7 +25,10 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.oauth2.VKPro import play.api.libs.json.{ JsObject, Json } import play.api.mvc.AnyContentAsEmpty import play.api.test.{ FakeRequest, WithApplication } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers._ import test.Helper +import test.Helper.mock import scala.concurrent.{ ExecutionContext, Future } @@ -36,198 +39,222 @@ class VKProviderSpec extends OAuth2ProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = provider.withSettings { s => - s.copy(accessTokenURL = "new-access-token-url") - } + override def running() = { + val s: VKProvider = provider.withSettings { s => + s.copy(accessTokenURL = "new-access-token-url") + } - s.settings.accessTokenURL must be equalTo "new-access-token-url" + s.settings.accessTokenURL must be equalTo "new-access-token-url" + } } } "The `authenticate` method" should { "fail with UnexpectedResponseException for an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 401 - wsResponse.body returns "Unauthorized" - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(401) + when(wsResponse.body).thenReturn("Unauthorized") + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(UnexpectedResponse.format(provider.id, "Unauthorized", 401)) + } } } "fail with UnexpectedResponseException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns Json.obj() - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - failed[UnexpectedResponseException](provider.authenticate()) { - case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Json.obj()) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + failed[UnexpectedResponseException](provider.authenticate()) { + case e => e.getMessage must startWith(InvalidInfoFormat.format(provider.id, "")) + } } } "return the auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - - authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + + authInfo(provider.authenticate())(_ must be equalTo oAuthInfo.as[OAuth2Info]) + } } } "The `authenticate` method with user state" should { "return stateful auth info" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") - wsResponse.status returns 200 - wsResponse.json returns oAuthInfo - wsRequest.withHttpHeaders(any) returns wsRequest - wsRequest.post[Map[String, Seq[String]]](any)(any) returns Future.successful(wsResponse) - httpLayer.url(oAuthSettings.accessTokenURL) returns wsRequest - stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext]) returns Future.successful(state) - stateProvider.state(any[ExecutionContext]) returns Future.successful(state) - stateProvider.withHandler(any[SocialStateItemHandler]) returns stateProvider - state.items returns Set(userStateItem) - - statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + implicit val req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "?" + Code + "=my.code") + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(oAuthInfo) + when(wsRequest.withHttpHeaders(any)).thenReturn(wsRequest) + when(wsRequest.post[Map[String, Seq[String]]](any)(any)).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(oAuthSettings.accessTokenURL)).thenReturn(wsRequest) + when(stateProvider.unserialize(anyString)(any[ExtractableRequest[String]], any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.state(any[ExecutionContext])).thenReturn(Future.successful(state)) + when(stateProvider.withHandler(any[SocialStateItemHandler])).thenReturn(stateProvider) + when(state.items).thenReturn(Set(userStateItem)) + + statefulAuthInfo(provider.authenticate(userStateItem))(_ must be equalTo stateAuthInfo) + } } } "The `retrieveProfile` method" should { "fail with ProfileRetrievalException if API returns error" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 400 - wsResponse.json returns Helper.loadJson("providers/oauth2/vk.error.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(SpecifiedProfileError.format( - provider.id, - 10, - "Internal server error: could not get application")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(400) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/vk.error.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(SpecifiedProfileError.format( + provider.id, + 10, + "Internal server error: could not get application")) + } } } "fail with ProfileRetrievalException if an unexpected error occurred" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 500 - wsResponse.json throws new RuntimeException("") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { - case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(500) + when(wsResponse.json).thenThrow(new RuntimeException("")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { + case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id)) + } } } "use the overridden API URL" in new WithApplication with Context { - val url = "https://custom.api.url?access_token=%s" - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - oAuthSettings.apiURL returns Some(url) - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/vk.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(url.format("my.access.token")) returns wsRequest - - await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) - - there was one(httpLayer).url(url.format("my.access.token")) + override def running() = { + val url = "https://custom.api.url?access_token=%s" + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(oAuthSettings.apiURL).thenReturn(Some(url)) + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/vk.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(url.format("my.access.token"))).thenReturn(wsRequest) + + await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) + + verify(httpLayer).url(url.format("my.access.token")) + } } "return the social profile with email" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/vk.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "66748"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("http://vk.com/images/camera_b.gif")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/vk.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "66748"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("http://vk.com/images/camera_b.gif")) + } } } "return the social profile from response without photo" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/vk.success.without.photo.json").as[JsObject] - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "66748"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = None) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/vk.success.without.photo.json").as[JsObject]) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "66748"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = None) + } } } "return the social profile from deprecated API response" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/vk.success.deprecated.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "66748"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("http://vk.com/images/camera_b.gif")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/vk.success.deprecated.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "66748"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("http://vk.com/images/camera_b.gif")) + } } } "return the social profile without email" in new WithApplication with Context { - val wsRequest = mock[MockWSRequest] - val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsResponse.json returns Helper.loadJson("providers/oauth2/vk.success.json") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(API.format("my.access.token")) returns wsRequest - - profile(provider.retrieveProfile((oAuthInfo.as[JsObject] - "email").as[OAuth2Info])) { p => - p must be equalTo CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "66748"), - firstName = Some("Apollonia"), - lastName = Some("Vanova"), - email = None, - avatarURL = Some("http://vk.com/images/camera_b.gif")) + override def running() = { + val wsRequest = mock[MockWSRequest] + val wsResponse = mock[MockWSRequest#Response] + when(wsResponse.status).thenReturn(200) + when(wsResponse.json).thenReturn(Helper.loadJson("providers/oauth2/vk.success.json")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(API.format("my.access.token"))).thenReturn(wsRequest) + + profile(provider.retrieveProfile((oAuthInfo.as[JsObject] - "email").as[OAuth2Info])) { p => + p must be equalTo CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "66748"), + firstName = Some("Apollonia"), + lastName = Some("Vanova"), + email = None, + avatarURL = Some("http://vk.com/images/camera_b.gif")) + } } } } @@ -270,6 +297,6 @@ class VKProviderSpec extends OAuth2ProviderSpec { /** * The provider to test. */ - lazy val provider = new VKProvider(httpLayer, stateProvider, oAuthSettings) + lazy val provider: VKProvider = new VKProvider(httpLayer, stateProvider, oAuthSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/SteamProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/SteamProviderSpec.scala index ff97f310..cd44acaa 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/SteamProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/SteamProviderSpec.scala @@ -18,6 +18,7 @@ package io.github.honeycombcheesecake.play.silhouette.impl.providers.openid import io.github.honeycombcheesecake.play.silhouette.api.LoginInfo import io.github.honeycombcheesecake.play.silhouette.impl.providers._ import play.api.test.WithApplication +import org.mockito.Mockito._ /** * Test case for the [[SteamProvider]] class. @@ -26,21 +27,25 @@ class SteamProviderSpec extends OpenIDProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val overrideSettingsFunction: OpenIDSettings => OpenIDSettings = { s => - s.copy("new-provider-url") - } - val s = provider.withSettings(overrideSettingsFunction) + override def running() = { + val overrideSettingsFunction: OpenIDSettings => OpenIDSettings = { s => + s.copy("new-provider-url") + } + val s: SteamProvider = provider.withSettings(overrideSettingsFunction) - s.settings.providerURL must be equalTo "new-provider-url" - there was one(openIDService).withSettings(overrideSettingsFunction) + s.settings.providerURL must be equalTo "new-provider-url" + verify(openIDService).withSettings(overrideSettingsFunction) + } } } "The `retrieveProfile` method" should { "return the social profile" in new WithApplication with Context { - profile(provider.retrieveProfile(openIDInfo)) { - case p => p must be equalTo new CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "http://steamcommunity.com/openid/id/16261495063738643")) + override def running() = { + profile(provider.retrieveProfile(openIDInfo)) { + case p => p must be equalTo new CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "http://steamcommunity.com/openid/id/16261495063738643")) + } } } } @@ -73,6 +78,6 @@ class SteamProviderSpec extends OpenIDProviderSpec { /** * The provider to test. */ - lazy val provider = new SteamProvider(httpLayer, openIDService, openIDSettings) + lazy val provider: SteamProvider = new SteamProvider(httpLayer, openIDService, openIDSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/YahooProviderSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/YahooProviderSpec.scala index 8c5581ed..213e5ba6 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/YahooProviderSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/YahooProviderSpec.scala @@ -18,6 +18,7 @@ package io.github.honeycombcheesecake.play.silhouette.impl.providers.openid import io.github.honeycombcheesecake.play.silhouette.api.LoginInfo import io.github.honeycombcheesecake.play.silhouette.impl.providers._ import play.api.test.WithApplication +import org.mockito.Mockito._ /** * Test case for the [[YahooProvider]] class. @@ -26,25 +27,29 @@ class YahooProviderSpec extends OpenIDProviderSpec { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val overrideSettingsFunction: OpenIDSettings => OpenIDSettings = { s => - s.copy("new-provider-url") - } - val s = provider.withSettings(overrideSettingsFunction) + override def running() = { + val overrideSettingsFunction: OpenIDSettings => OpenIDSettings = { s => + s.copy("new-provider-url") + } + val s: YahooProvider = provider.withSettings(overrideSettingsFunction) - s.settings.providerURL must be equalTo "new-provider-url" - there was one(openIDService).withSettings(overrideSettingsFunction) + s.settings.providerURL must be equalTo "new-provider-url" + verify(openIDService).withSettings(overrideSettingsFunction) + } } } "The `retrieveProfile` method" should { "return the social profile" in new WithApplication with Context { - profile(provider.retrieveProfile(openIDInfo)) { - case p => - p must be equalTo new CommonSocialProfile( - loginInfo = LoginInfo(provider.id, "https://me.yahoo.com/a/Xs6hPjazdrMvmbn4jhQjkjkhcasdGdsKajq9we"), - fullName = Some("Apollonia Vanova"), - email = Some("apollonia.vanova@watchmen.com"), - avatarURL = Some("https://s.yimg.com/dh/ap/social/profile/profile_b48.png")) + override def running() = { + profile(provider.retrieveProfile(openIDInfo)) { + case p => + p must be equalTo new CommonSocialProfile( + loginInfo = LoginInfo(provider.id, "https://me.yahoo.com/a/Xs6hPjazdrMvmbn4jhQjkjkhcasdGdsKajq9we"), + fullName = Some("Apollonia Vanova"), + email = Some("apollonia.vanova@watchmen.com"), + avatarURL = Some("https://s.yimg.com/dh/ap/social/profile/profile_b48.png")) + } } } } @@ -84,6 +89,6 @@ class YahooProviderSpec extends OpenIDProviderSpec { /** * The provider to test. */ - lazy val provider = new YahooProvider(httpLayer, openIDService, openIDSettings) + lazy val provider: YahooProvider = new YahooProvider(httpLayer, openIDService, openIDSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/service/PlayOpenIDServiceSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/service/PlayOpenIDServiceSpec.scala index d4c232d3..0993854c 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/service/PlayOpenIDServiceSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/openid/service/PlayOpenIDServiceSpec.scala @@ -2,20 +2,22 @@ package io.github.honeycombcheesecake.play.silhouette.impl.providers.openid.serv import io.github.honeycombcheesecake.play.silhouette.impl.providers.OpenIDSettings import io.github.honeycombcheesecake.play.silhouette.impl.providers.openid.services.PlayOpenIDService -import org.specs2.mock.Mockito import org.specs2.specification.Scope +import org.mockito.Mockito.mock import play.api.libs.openid.OpenIdClient import play.api.test.{ PlaySpecification, WithApplication } -class PlayOpenIDServiceSpec extends PlaySpecification with Mockito { +class PlayOpenIDServiceSpec extends PlaySpecification { "The `withSettings` method" should { "create a new instance with customized settings" in new WithApplication with Context { - val s = service.withSettings { s => - s.copy("new-provider-url") - } + override def running() = { + val s = service.withSettings { s => + s.copy("new-provider-url") + } - s.settings.providerURL must be equalTo "new-provider-url" + s.settings.providerURL must be equalTo "new-provider-url" + } } } @@ -35,7 +37,7 @@ class PlayOpenIDServiceSpec extends PlaySpecification with Mockito { "image" -> "http://axschema.org/media/image/default"), realm = Some("http://localhost:9000")) - val service = new PlayOpenIDService(mock[OpenIdClient], openIDSettings) + lazy val service = new PlayOpenIDService(mock(classOf[OpenIdClient]), openIDSettings) } } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala index 06473612..e7d22297 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala @@ -21,11 +21,13 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialStateI import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialStateItem.ItemStructure import io.github.honeycombcheesecake.play.silhouette.impl.providers.state.CsrfStateItemHandler._ import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.json.Json import play.api.mvc.{ AnyContentAsEmpty, Cookie, Results } import play.api.test.{ FakeRequest, PlaySpecification } +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mockSmart import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -34,11 +36,11 @@ import scala.util.Success /** * Test case for the [[CsrfStateItemHandler]] class. */ -class CsrfStateItemHandlerSpec extends PlaySpecification with Mockito with JsonMatchers { +class CsrfStateItemHandlerSpec extends PlaySpecification with JsonMatchers { "The `item` method" should { "return the CSRF state item" in new Context { - idGenerator.generate returns Future.successful(csrfToken) + when(idGenerator.generate).thenReturn(Future.successful(csrfToken)) await(csrfStateItemHandler.item) must be equalTo csrfStateItem } @@ -50,7 +52,7 @@ class CsrfStateItemHandlerSpec extends PlaySpecification with Mockito with JsonM } "should return `None` if it can't handle the given item" in new Context { - val nonCsrfState = mock[SocialStateItem].smart + val nonCsrfState = mockSmart[SocialStateItem] csrfStateItemHandler.canHandle(nonCsrfState) must beNone } @@ -58,8 +60,8 @@ class CsrfStateItemHandlerSpec extends PlaySpecification with Mockito with JsonM "The `canHandle` method" should { "return false if the give item is for another handler" in new Context { - val nonCsrfItemStructure = mock[ItemStructure].smart - nonCsrfItemStructure.id returns "non-csrf-item" + val nonCsrfItemStructure = mockSmart[ItemStructure] + when(nonCsrfItemStructure.id).thenReturn("non-csrf-item") implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() csrfStateItemHandler.canHandle(nonCsrfItemStructure) must beFalse @@ -107,7 +109,7 @@ class CsrfStateItemHandlerSpec extends PlaySpecification with Mockito with JsonM /** * The ID generator implementation. */ - val idGenerator = mock[IDGenerator].smart + val idGenerator = mockSmart[IDGenerator] /** * The settings. @@ -120,9 +122,9 @@ class CsrfStateItemHandlerSpec extends PlaySpecification with Mockito with JsonM * The signer returns the same value as passed to the methods. This is enough for testing. */ val signer = { - val c = mock[Signer].smart - c.sign(any()) answers { p: Any => p.asInstanceOf[String] } - c.extract(any()) answers { p: Any => Success(p.asInstanceOf[String]) } + val c = mockSmart[Signer] + when(c.sign(any())).thenAnswer(_.getArgument(0).asInstanceOf[String]) + when(c.extract(any())).thenAnswer(p => Success(p.getArgument(0).asInstanceOf[String])) c } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala index 6521ca43..9cfb7336 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala @@ -19,18 +19,19 @@ import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialStateI import io.github.honeycombcheesecake.play.silhouette.impl.providers.SocialStateItem.ItemStructure import io.github.honeycombcheesecake.play.silhouette.impl.providers.state.UserStateItemHandler._ import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.json.Json import play.api.mvc.AnyContentAsEmpty import play.api.test.{ FakeRequest, PlaySpecification } +import org.mockito.Mockito._ +import test.Helper.mockSmart import scala.concurrent.ExecutionContext.Implicits.global /** * Test case for the [[UserStateItemHandler]] class. */ -class UserStateItemHandlerSpec extends PlaySpecification with Mockito with JsonMatchers { +class UserStateItemHandlerSpec extends PlaySpecification with JsonMatchers { "The `item` method" should { "return the user state item" in new Context { @@ -44,7 +45,7 @@ class UserStateItemHandlerSpec extends PlaySpecification with Mockito with JsonM } "should return `None` if it can't handle the given item" in new Context { - val nonUserState = mock[SocialStateItem].smart + val nonUserState = mockSmart[SocialStateItem] userStateItemHandler.canHandle(nonUserState) must beNone } @@ -52,8 +53,8 @@ class UserStateItemHandlerSpec extends PlaySpecification with Mockito with JsonM "The `canHandle` method" should { "return false if the give item is for another handler" in new Context { - val nonUserItemStructure = mock[ItemStructure].smart - nonUserItemStructure.id returns "non-user-item" + val nonUserItemStructure = mockSmart[ItemStructure] + when(nonUserItemStructure.id).thenReturn("non-user-item") implicit val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() userStateItemHandler.canHandle(nonUserItemStructure) must beFalse diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/services/GravatarServiceSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/services/GravatarServiceSpec.scala index ca5b6a67..11af505a 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/services/GravatarServiceSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/services/GravatarServiceSpec.scala @@ -17,9 +17,11 @@ package io.github.honeycombcheesecake.play.silhouette.impl.services import io.github.honeycombcheesecake.play.silhouette.api.util.{ MockHTTPLayer, MockWSRequest } import io.github.honeycombcheesecake.play.silhouette.impl.services.GravatarService._ -import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.test.PlaySpecification +import org.mockito.Mockito._ +import org.mockito.ArgumentMatchers.any +import test.Helper.mock import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -27,7 +29,7 @@ import scala.concurrent.Future /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.services.GravatarService]] class. */ -class GravatarServiceSpec extends PlaySpecification with Mockito { +class GravatarServiceSpec extends PlaySpecification { "The `retrieveURL` method" should { "return None if email is empty" in new Context { @@ -38,9 +40,9 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { val wsRequest = mock[MockWSRequest] val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 404 - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(any) returns wsRequest + when(wsResponse.status).thenReturn(404) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(any)).thenReturn(wsRequest) await(service.retrieveURL(email)) should beNone } @@ -49,9 +51,9 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { val wsRequest = mock[MockWSRequest] val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 404 - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(any) returns wsRequest + when(wsResponse.status).thenReturn(404) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(any)).thenReturn(wsRequest) await(service.retrieveURL(email)) should beNone } @@ -60,9 +62,9 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { val wsRequest = mock[MockWSRequest] val wsResponse = mock[MockWSRequest#Response] - wsResponse.status throws new RuntimeException("Timeout error") - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(any) returns wsRequest + when(wsResponse.status).thenThrow(new RuntimeException("Timeout error")) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(any)).thenReturn(wsRequest) await(service.retrieveURL(email)) should beNone } @@ -71,9 +73,9 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { val wsRequest = mock[MockWSRequest] val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(any) returns wsRequest + when(wsResponse.status).thenReturn(200) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(any)).thenReturn(wsRequest) await(service.retrieveURL(email)) should beSome(SecureURL.format(hash, "?d=404")) } @@ -82,10 +84,10 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { val wsRequest = mock[MockWSRequest] val wsResponse = mock[MockWSRequest#Response] - settings.secure returns false - wsResponse.status returns 200 - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(any) returns wsRequest + when(settings.secure).thenReturn(false) + when(wsResponse.status).thenReturn(200) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(any)).thenReturn(wsRequest) await(service.retrieveURL(email)) should beSome(InsecureURL.format(hash, "?d=404")) } @@ -94,10 +96,10 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { val wsRequest = mock[MockWSRequest] val wsResponse = mock[MockWSRequest#Response] - settings.params returns Map("d" -> "http://example.com/images/avatar.jpg", "s" -> "400") - wsResponse.status returns 200 - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(any) returns wsRequest + when(settings.params).thenReturn(Map("d" -> "http://example.com/images/avatar.jpg", "s" -> "400")) + when(wsResponse.status).thenReturn(200) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(any)).thenReturn(wsRequest) await(service.retrieveURL(email)) should beSome( SecureURL.format(hash, "?d=http%3A%2F%2Fexample.com%2Fimages%2Favatar.jpg&s=400")) @@ -107,9 +109,9 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { val wsRequest = mock[MockWSRequest] val wsResponse = mock[MockWSRequest#Response] - wsResponse.status returns 200 - wsRequest.get() returns Future.successful(wsResponse) - httpLayer.url(any) returns wsRequest + when(wsResponse.status).thenReturn(200) + when(wsRequest.get()).thenReturn(Future.successful(wsResponse)) + when(httpLayer.url(any)).thenReturn(wsRequest) await(service.retrieveURL("123test@test.com")) should beSome( SecureURL.format("0d77aed6b4c5857473c9a04c2017f8b8", "?d=404")) @@ -126,7 +128,7 @@ class GravatarServiceSpec extends PlaySpecification with Mockito { */ val httpLayer = { val m = mock[MockHTTPLayer] - m.executionContext returns global + when(m.executionContext).thenReturn(global) m } diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/DefaultFingerprintGeneratorSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/DefaultFingerprintGeneratorSpec.scala index 1a463dd8..3ef16ea7 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/DefaultFingerprintGeneratorSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/DefaultFingerprintGeneratorSpec.scala @@ -27,7 +27,7 @@ class DefaultFingerprintGeneratorSpec extends PlaySpecification { "return fingerprint including the `User-Agent` header" in { val userAgent = "test-user-agent" val generator = new DefaultFingerprintGenerator() - implicit val request = FakeRequest().withHeaders(USER_AGENT -> userAgent) + implicit val request = FakeRequest().withHeaders((USER_AGENT, userAgent)) generator.generate must be equalTo Hash.sha1(userAgent + ":::") } @@ -35,7 +35,7 @@ class DefaultFingerprintGeneratorSpec extends PlaySpecification { "return fingerprint including the `Accept-Language` header" in { val acceptLanguage = "test-accept-language" val generator = new DefaultFingerprintGenerator() - implicit val request = FakeRequest().withHeaders(ACCEPT_LANGUAGE -> acceptLanguage) + implicit val request = FakeRequest().withHeaders((ACCEPT_LANGUAGE, acceptLanguage)) generator.generate must be equalTo Hash.sha1(":" + acceptLanguage + "::") } @@ -43,7 +43,7 @@ class DefaultFingerprintGeneratorSpec extends PlaySpecification { "return fingerprint including the `Accept-Charset` header" in { val acceptCharset = "test-accept-charset" val generator = new DefaultFingerprintGenerator() - implicit val request = FakeRequest().withHeaders(ACCEPT_CHARSET -> acceptCharset) + implicit val request = FakeRequest().withHeaders((ACCEPT_CHARSET, acceptCharset)) generator.generate must be equalTo Hash.sha1("::" + acceptCharset + ":") } @@ -61,9 +61,9 @@ class DefaultFingerprintGeneratorSpec extends PlaySpecification { val acceptCharset = "test-accept-charset" val generator = new DefaultFingerprintGenerator(true) implicit val request = FakeRequest().withHeaders( - USER_AGENT -> userAgent, - ACCEPT_LANGUAGE -> acceptLanguage, - ACCEPT_CHARSET -> acceptCharset) + (USER_AGENT, userAgent), + (ACCEPT_LANGUAGE, acceptLanguage), + (ACCEPT_CHARSET, acceptCharset)) generator.generate must be equalTo Hash.sha1( userAgent + ":" + acceptLanguage + ":" + acceptCharset + ":127.0.0.1") diff --git a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/PlayCacheLayerSpec.scala b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/PlayCacheLayerSpec.scala index 56510f0f..a5c63671 100644 --- a/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/PlayCacheLayerSpec.scala +++ b/silhouette/test/io/github/honeycombcheesecake/play/silhouette/impl/util/PlayCacheLayerSpec.scala @@ -15,7 +15,7 @@ */ package io.github.honeycombcheesecake.play.silhouette.impl.util -import org.specs2.mock.Mockito +import org.mockito.Mockito._ import org.specs2.specification.Scope import play.api.cache.AsyncCacheApi import play.api.test.PlaySpecification @@ -27,15 +27,15 @@ import scala.concurrent.duration.Duration /** * Test case for the [[io.github.honeycombcheesecake.play.silhouette.impl.util.PlayCacheLayer]] class. */ -class PlayCacheLayerSpec extends PlaySpecification with Mockito { +class PlayCacheLayerSpec extends PlaySpecification { "The `find` method" should { "return value from cache" in new Context { - cacheAPI.get[ZonedDateTime]("id") returns Future.successful(Some(value)) + when(cacheAPI.get[ZonedDateTime]("id")).thenReturn(Future.successful(Some(value))) await(layer.find[ZonedDateTime]("id")) should beSome(value) - there was one(cacheAPI).get[ZonedDateTime]("id") + verify(cacheAPI).get[ZonedDateTime]("id") } } @@ -43,7 +43,7 @@ class PlayCacheLayerSpec extends PlaySpecification with Mockito { "save value in cache" in new Context { await(layer.save("id", value)) - there was one(cacheAPI).set("id", value, Duration.Inf) + verify(cacheAPI).set("id", value, Duration.Inf) } } @@ -51,7 +51,7 @@ class PlayCacheLayerSpec extends PlaySpecification with Mockito { "removes value from cache" in new Context { await(layer.remove("id")) must beEqualTo(()) - there was one(cacheAPI).remove("id") + verify(cacheAPI).remove("id") } } @@ -63,7 +63,7 @@ class PlayCacheLayerSpec extends PlaySpecification with Mockito { /** * The cache API. */ - lazy val cacheAPI = mock[AsyncCacheApi] + lazy val cacheAPI = mock(classOf[AsyncCacheApi]) /** * The layer to test.