~jan0sch/smederee
Showing details for patch dbdefe8d234d4eebb2578b39cb77733a07307444.
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala 2025-01-11 03:11:16.720535197 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala 2025-01-11 03:11:16.720535197 +0000 @@ -13,6 +13,7 @@ import cats.syntax.all.* import com.typesafe.config.ConfigFactory import de.smederee.hub.Generators.* +import de.smederee.hub.Generators.given import de.smederee.hub.config.* import de.smederee.hub.forms.* import de.smederee.hub.forms.types.* @@ -25,7 +26,11 @@ import munit.* -class AuthenticationRoutesTest extends CatsEffectSuite { +import org.scalacheck.effect.PropF + +class AuthenticationRoutesTest extends CatsEffectSuite with ScalaCheckEffectSuite { + override def scalaCheckTestParameters = super.scalaCheckTestParameters.withMinSuccessfulTests(1) + val loginPath = uri"/login" private val resetPath = uri"/forgot-password" @@ -152,198 +157,190 @@ } test("POST /login must return 400 - Bad Request AND increase failed attempts if the credentials are invalid") { - genValidAccount.sample match { - case None => fail("Could not generate data samples!") - case Some(account) => - val clock = java.time.Clock.systemUTC - val authenticationConfig = configuration.service.authentication - val externalConfig = configuration.service.external - val repo = new TestAuthenticationRepository[IO](List(account), List.empty) - val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) - - def service: HttpRoutes[IO] = - Router( - "/" -> new AuthenticationRoutes[IO]( - clock, - authenticationConfig, - externalConfig, - repo, - signAndValidate - ).routes - ) - - val payload = UrlForm.empty - .updateFormField(LoginForm.fieldName.toString, account.name.toString) - .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter") - def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) - - val expectedHtml = - views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)( - formData = Map(LoginForm.fieldName.toString -> Chain(account.name.toString)), - Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!"))) - ) - - def response: IO[Response[IO]] = service.orNotFound.run(request) - - val test = for { - result <- response - body <- result.as[String] - failed <- repo.getFailed - } yield (result, body, failed) - - test.map { output => - val (result, body, failed) = output - assertEquals(result.status, Status.BadRequest) - assertEquals(failed.find(_._1 === account.uid), Option((account.uid, 1))) - assertEquals(body, expectedHtml.toString) - } + PropF.forAllF { (account: Account) => + val clock = java.time.Clock.systemUTC + val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external + val repo = new TestAuthenticationRepository[IO](List(account), List.empty) + val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) + + def service: HttpRoutes[IO] = + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) + + val payload = UrlForm.empty + .updateFormField(LoginForm.fieldName.toString, account.name.toString) + .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter") + def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) + + val expectedHtml = + views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)( + formData = Map(LoginForm.fieldName.toString -> Chain(account.name.toString)), + Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!"))) + ) + + def response: IO[Response[IO]] = service.orNotFound.run(request) + + val test = for { + result <- response + body <- result.as[String] + failed <- repo.getFailed + } yield (result, body, failed) + + test.start.flatMap(_.joinWithNever).map { output => + val (result, body, failed) = output + assertEquals(result.status, Status.BadRequest) + assertEquals(failed.find(_._1 === account.uid), Option((account.uid, 1))) + assertEquals(body, expectedHtml.toString) + } } } test( "POST /login must return 400 - Bad Request AND lock the account if the credentials are invalid and failed attempts are too high" ) { - genValidAccount.sample match { - case None => fail("Could not generate data samples!") - case Some(account) => - val clock = java.time.Clock.systemUTC - val authenticationConfig = configuration.service.authentication - val externalConfig = configuration.service.external - val repo = new TestAuthenticationRepository[IO](List(account), List.empty) - val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) - - for (_ <- 0 to authenticationConfig.lockAfter.toInt) yield repo.incrementFailedAttempts(account.uid) - - def service: HttpRoutes[IO] = - Router( - "/" -> new AuthenticationRoutes[IO]( - clock, - authenticationConfig, - externalConfig, - repo, - signAndValidate - ).routes - ) - - val payload = UrlForm.empty - .updateFormField(LoginForm.fieldName.toString, account.name.toString) - .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter") - def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) - - val expectedHtml = - views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)( - formData = Map(LoginForm.fieldName.toString -> Chain(account.name.toString)), - Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!"))) - ) - - def response: IO[Response[IO]] = service.orNotFound.run(request) - - val test = for { - result <- response - body <- result.as[String] - locked <- repo.getLocked - } yield (result, body, locked) - - test.map { output => - val (result, body, locked) = output - assertEquals(result.status, Status.BadRequest) - assert(locked.exists(_ === account.uid), s"Account was not locked! ($locked)") - assertEquals(body, expectedHtml.toString) - } + PropF.forAllF { (account: Account) => + val clock = java.time.Clock.systemUTC + val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external + val repo = new TestAuthenticationRepository[IO](List(account), List.empty) + val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) + + for (_ <- 0 to authenticationConfig.lockAfter.toInt) yield repo.incrementFailedAttempts(account.uid) + + def service: HttpRoutes[IO] = + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) + + val payload = UrlForm.empty + .updateFormField(LoginForm.fieldName.toString, account.name.toString) + .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter") + def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) + + val expectedHtml = + views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)( + formData = Map(LoginForm.fieldName.toString -> Chain(account.name.toString)), + Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!"))) + ) + + def response: IO[Response[IO]] = service.orNotFound.run(request) + + val test = for { + result <- response + body <- result.as[String] + locked <- repo.getLocked + } yield (result, body, locked) + + test.start.flatMap(_.joinWithNever).map { output => + val (result, body, locked) = output + assertEquals(result.status, Status.BadRequest) + assert(locked.exists(_ === account.uid), s"Account was not locked! ($locked)") + assertEquals(body, expectedHtml.toString) + } } } test("POST /login must return 400 - Bad Request if the credentials are valid but the account is locked") { - genValidAccount.sample match { - case None => fail("Could not generate data samples!") - case Some(account) => - val clock = java.time.Clock.systemUTC - val authenticationConfig = configuration.service.authentication - val externalConfig = configuration.service.external - // A locked account is not supposed to be returned by findAccount, so we leave the account list empty. - val repo = new TestAuthenticationRepository[IO](List.empty, List.empty) - val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) - - def service: HttpRoutes[IO] = - Router( - "/" -> new AuthenticationRoutes[IO]( - clock, - authenticationConfig, - externalConfig, - repo, - signAndValidate - ).routes - ) - - val payload = UrlForm.empty - .updateFormField(LoginForm.fieldName.toString, account.name.toString) - .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter") - def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) - - val expectedHtml = - views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)( - formData = Map(LoginForm.fieldName.toString -> Chain(account.name.toString)), - Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!"))) - ) - - def response: IO[Response[IO]] = service.orNotFound.run(request) - - val test = for { - result <- response - body <- result.as[String] - } yield (result, body) - - test.map { output => - val (result, body) = output - assertEquals(result.status, Status.BadRequest) - assertEquals(body, expectedHtml.toString) - } + PropF.forAllF { (account: Account) => + val clock = java.time.Clock.systemUTC + val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external + // A locked account is not supposed to be returned by findAccount, so we leave the account list empty. + val repo = new TestAuthenticationRepository[IO](List.empty, List.empty) + val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) + + def service: HttpRoutes[IO] = + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) + + val payload = UrlForm.empty + .updateFormField(LoginForm.fieldName.toString, account.name.toString) + .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter") + def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) + + val expectedHtml = + views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)( + formData = Map(LoginForm.fieldName.toString -> Chain(account.name.toString)), + Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!"))) + ) + + def response: IO[Response[IO]] = service.orNotFound.run(request) + + val test = for { + result <- response + body <- result.as[String] + } yield (result, body) + + test.start.flatMap(_.joinWithNever).map { output => + val (result, body) = output + assertEquals(result.status, Status.BadRequest) + assertEquals(body, expectedHtml.toString) + } } } test("POST /login must return 303 - See Other AND set the authentication cookie if the credentials are valid") { - genValidAccount.sample match { - case None => fail("Could not generate data samples!") - case Some(account) => - val clock = java.time.Clock.systemUTC - val authenticationConfig = configuration.service.authentication - val externalConfig = configuration.service.external - val repo = new TestAuthenticationRepository[IO](List(account), List.empty) - val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) - - def service: HttpRoutes[IO] = - Router( - "/" -> new AuthenticationRoutes[IO]( - clock, - authenticationConfig, - externalConfig, - repo, - signAndValidate - ).routes - ) - - val payload = UrlForm.empty - .updateFormField(LoginForm.fieldName.toString, account.name.toString) - .updateFormField(LoginForm.fieldPassword.toString, repo.DefaultPassword) - def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) - - def response: IO[Response[IO]] = service.orNotFound.run(request) - - val test = for { - result <- response - body <- result.as[String] - } yield (result, body) - - test.map { output => - val (result, body) = output - assertEquals(result.status, Status.SeeOther) - assert(body.isEmpty, "Response body must be empty!") - result.cookies.find(_.name === Constants.authenticationCookieName.toString).map(_.content) match { - case None => fail("Authentication cookie not set!") - case Some(content) => - assert(SignedToken.from(content).flatMap(signAndValidate.validate).nonEmpty) - } + PropF.forAllF { (account: Account) => + val clock = java.time.Clock.systemUTC + val authenticationConfig = configuration.service.authentication + val externalConfig = configuration.service.external + val repo = new TestAuthenticationRepository[IO](List(account), List.empty) + val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret) + + def service: HttpRoutes[IO] = + Router( + "/" -> new AuthenticationRoutes[IO]( + clock, + authenticationConfig, + externalConfig, + repo, + signAndValidate + ).routes + ) + + val payload = UrlForm.empty + .updateFormField(LoginForm.fieldName.toString, account.name.toString) + .updateFormField(LoginForm.fieldPassword.toString, repo.DefaultPassword) + def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload) + + def response: IO[Response[IO]] = service.orNotFound.run(request) + + val test = for { + result <- response + body <- result.as[String] + } yield (result, body) + + test.start.flatMap(_.joinWithNever).map { output => + val (result, body) = output + assertEquals(result.status, Status.SeeOther) + assert(body.isEmpty, "Response body must be empty!") + result.cookies.find(_.name === Constants.authenticationCookieName.toString).map(_.content) match { + case None => fail("Authentication cookie not set!") + case Some(content) => + assert(SignedToken.from(content).flatMap(signAndValidate.validate).nonEmpty) } + } } } }