Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to cats-effect 3 #64

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ ThisBuild / githubWorkflowEnv ++= List("PGP_PASSPHRASE", "PGP_SECRET", "SONATYPE

val Versions = new {
val catsCore = "2.6.1"
val catsEffect = "2.3.1"
val catsEffect = "3.1.1"
val circe = "0.13.0"
val kindProjector = "0.13.0"
val monix = "3.4.0"
val scalaTest = "3.2.9"
val sttp = "3.2.3"
val sttp = "3.3.5"
val refined = "0.9.25"
}

Expand All @@ -77,7 +77,7 @@ val testDependencies = Seq(
).map(_ % Test)

val mimaSettings =
mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% moduleName.value % _).toSet
mimaPreviousArtifacts := Set() // previousStableVersion.value.map(organization.value %% moduleName.value % _).toSet

lazy val oauth2 = project.settings(
name := "sttp-oauth2",
Expand Down Expand Up @@ -115,7 +115,9 @@ lazy val `oauth2-backend-cats` = project
.settings(
name := "sttp-oauth2-backend-cats",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % Versions.catsEffect,
"org.typelevel" %% "cats-effect-std" % Versions.catsEffect,
"org.typelevel" %% "cats-effect" % Versions.catsEffect % Test,
"org.typelevel" %% "cats-effect-testkit" % Versions.catsEffect % Test,
"com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % Versions.sttp % Test
) ++ plugins ++ testDependencies,
mimaSettings
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.ocadotechnology.sttp.oauth2.backend

import cats.effect.Sync
import cats.effect.concurrent.Ref
import cats.Functor
import cats.effect.kernel.Ref
import cats.implicits._

final class CatsRefCache[F[_], A] private (ref: Ref[F, Option[A]]) extends Cache[F, A] {
Expand All @@ -11,5 +11,5 @@ final class CatsRefCache[F[_], A] private (ref: Ref[F, Option[A]]) extends Cache

object CatsRefCache {

def apply[F[_]: Sync, A]: F[Cache[F, A]] = Ref[F].of(Option.empty[A]).map(new CatsRefCache(_))
def apply[F[_]: Ref.Make: Functor, A]: F[Cache[F, A]] = Ref[F].of(Option.empty[A]).map(new CatsRefCache(_))
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.ocadotechnology.sttp.oauth2.backend

import cats.Monad
import cats.data.OptionT
import cats.effect.Clock
import cats.effect.Concurrent
import cats.effect.concurrent.Semaphore
import cats.effect.kernel.Clock
import cats.effect.kernel.Concurrent
import cats.effect.kernel.MonadCancelThrow
import cats.effect.std.Semaphore
import cats.implicits._
import com.ocadotechnology.sttp.oauth2.ClientCredentialsProvider
import com.ocadotechnology.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
Expand All @@ -18,21 +18,21 @@ import sttp.model.Uri

import java.time.Instant

final class SttpOauth2ClientCredentialsCatsBackend[F[_]: Monad: Clock, P] private (
final class SttpOauth2ClientCredentialsCatsBackend[F[_]: Clock: MonadCancelThrow, P] private (
delegate: SttpBackend[F, P],
fetchTokenAction: F[AccessTokenResponse],
cache: Cache[F, TokenWithExpiryInstant],
semaphore: Semaphore[F]
) extends DelegateSttpBackend(delegate) {

override def send[T, R >: P with Effect[F]](request: Request[T, R]): F[Response[T]] = for {
token <- semaphore.withPermit(resolveToken)
token <- semaphore.permit.surround(resolveToken)
response <- delegate.send(request.auth.bearer(token.value))
} yield response

private val resolveToken: F[Secret[String]] =
OptionT(cache.get)
.product(OptionT.liftF(Clock[F].instantNow))
.product(OptionT.liftF(Clock[F].realTimeInstant))
.filter { case (TokenWithExpiryInstant(_, expiryInstant), currentInstant) => currentInstant.isBefore(expiryInstant) }
.map(_._1)
.getOrElseF(fetchAndSaveToken)
Expand All @@ -42,14 +42,14 @@ final class SttpOauth2ClientCredentialsCatsBackend[F[_]: Monad: Clock, P] privat
fetchTokenAction.flatMap(calculateExpiryInstant).flatTap(cache.set)

private def calculateExpiryInstant(response: AccessTokenResponse): F[TokenWithExpiryInstant] =
Clock[F].instantNow.map(_ plusMillis response.expiresIn.toMillis).map(TokenWithExpiryInstant(response.accessToken, _))
Clock[F].realTimeInstant.map(_ plusMillis response.expiresIn.toMillis).map(TokenWithExpiryInstant(response.accessToken, _))

}

object SttpOauth2ClientCredentialsCatsBackend {
final case class TokenWithExpiryInstant(token: Secret[String], expiryInstant: Instant)

def apply[F[_]: Concurrent: Clock, P](
def apply[F[_]: Clock: Concurrent, P](
tokenUrl: Uri,
tokenIntrospectionUrl: Uri,
clientId: NonEmptyString,
Expand All @@ -65,7 +65,7 @@ object SttpOauth2ClientCredentialsCatsBackend {

/** Keep in mind that the given implicit `backend` may be different than this one used by `clientCredentialsProvider`
*/
def usingClientCredentialsProvider[F[_]: Concurrent: Clock, P](
def usingClientCredentialsProvider[F[_]: Clock: Concurrent, P](
clientCredentialsProvider: ClientCredentialsProvider[F]
)(
scope: Scope
Expand All @@ -74,7 +74,7 @@ object SttpOauth2ClientCredentialsCatsBackend {
): F[SttpOauth2ClientCredentialsCatsBackend[F, P]] =
CatsRefCache[F, TokenWithExpiryInstant].flatMap(usingClientCredentialsProviderAndCache(clientCredentialsProvider, _)(scope))

def usingCache[F[_]: Concurrent: Clock, P](
def usingCache[F[_]: Clock: Concurrent, P](
cache: Cache[F, TokenWithExpiryInstant]
)(
tokenUrl: Uri,
Expand All @@ -92,7 +92,7 @@ object SttpOauth2ClientCredentialsCatsBackend {

/** Keep in mind that the given implicit `backend` may be different than this one used by `clientCredentialsProvider`
*/
def usingClientCredentialsProviderAndCache[F[_]: Concurrent: Clock, P](
def usingClientCredentialsProviderAndCache[F[_]: Clock: Concurrent, P](
clientCredentialsProvider: ClientCredentialsProvider[F],
cache: Cache[F, TokenWithExpiryInstant]
)(
Expand All @@ -104,12 +104,12 @@ object SttpOauth2ClientCredentialsCatsBackend {

/** Keep in mind that the given implicit `backend` may be different than this one used by `fetchTokenAction`
*/
def usingFetchTokenActionAndCache[F[_]: Concurrent: Clock, P](
def usingFetchTokenActionAndCache[F[_]: Clock: Concurrent, P](
fetchTokenAction: F[AccessTokenResponse],
cache: Cache[F, TokenWithExpiryInstant]
)(
implicit backend: SttpBackend[F, P]
): F[SttpOauth2ClientCredentialsCatsBackend[F, P]] =
Semaphore(n = 1).map(new SttpOauth2ClientCredentialsCatsBackend(backend, fetchTokenAction, cache, _))
Semaphore[F](n = 1).map(new SttpOauth2ClientCredentialsCatsBackend[F, P](backend, fetchTokenAction, cache, _))

}
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
package com.ocadotechnology.sttp.oauth2.backend

import cats.effect.ContextShift
import cats.effect.IO
import cats.effect.Timer
import cats.effect.kernel.Outcome
import cats.effect.kernel.testkit.TestContext
import cats.effect.testkit.TestInstances
import cats.implicits._
import com.ocadotechnology.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
import com.ocadotechnology.sttp.oauth2.Secret
import com.ocadotechnology.sttp.oauth2.common.Scope
import eu.timepit.refined.types.string.NonEmptyString
import org.scalatest.compatible.Assertion
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec
import org.scalatest.wordspec.AnyWordSpec
import sttp.client3._
import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend
import sttp.client3.testing.SttpBackendStub
import sttp.client3.testing._
import sttp.model.HeaderNames.Authorization
import sttp.model._

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matchers {
implicit override val executionContext: ExecutionContext = ExecutionContext.global
implicit val contextShift: ContextShift[IO] = IO.contextShift(executionContext)
implicit val timer: Timer[IO] = IO.timer(executionContext)
class SttpOauth2ClientCredentialsCatsBackendSpec extends AnyWordSpec with Matchers with TestInstances {

def ticked(f: IO[Assertion]): Assertion = {
implicit val ticker = Ticker(TestContext())

unsafeRun(f) shouldBe Outcome.succeeded(Some(succeed))
}

"SttpOauth2ClientCredentialsBackend" when {
val tokenUrl: Uri = uri"https://authserver.org/oauth2/token"
Expand All @@ -34,7 +38,7 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
val testAppUrl: Uri = uri"https://testapp.org/test"

"TestApp is invoked once" should {
"request a token. add the token to the TestApp request" in {
"request a token. add the token to the TestApp request" in ticked {
val accessToken: Secret[String] = Secret("token")
implicit val mockBackend: SttpBackendStub[IO, Any] = AsyncHttpClientCatsBackend
.stub[IO]
Expand All @@ -44,14 +48,16 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
.thenRespondOk()

for {
backend <- SttpOauth2ClientCredentialsCatsBackend[IO, Any](tokenUrl, uri"https://unused", clientId, clientSecret)(scope)
backend <-
SttpOauth2ClientCredentialsCatsBackend[IO, Any](tokenUrl, uri"https://unused", clientId, clientSecret)(scope)
response <- backend.send(basicRequest.get(testAppUrl).response(asStringAlways))
} yield response.code shouldBe StatusCode.Ok
}.unsafeToFuture()
}
}

"TestApp is invoked twice sequentially" should {
"first invocation is requesting a token, second invocation is getting the token from the cache. add the token to the both TestApp requests" in {
"first invocation is requesting a token, second invocation is getting the token from the cache. add the token to the both TestApp requests" in ticked {

val accessToken: Secret[String] = Secret("token")
implicit val recordingMockBackend: RecordingSttpBackend[IO, Any] = new RecordingSttpBackend(
AsyncHttpClientCatsBackend
Expand All @@ -72,11 +78,12 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
response2.code shouldBe StatusCode.Ok
recordingMockBackend.invocationCountByUri shouldBe Map(tokenUrl -> 1, testAppUrl -> 2)
}
}.unsafeToFuture()
}
}

"TestApp is invoked twice in parallel" should {
"first invocation is requesting a token, second invocation is waiting for token response and getting the token from the cache. add the token to the both TestApp requests" in {
"first invocation is requesting a token, second invocation is waiting for token response and getting the token from the cache. add the token to the both TestApp requests" in ticked {

val accessToken: Secret[String] = Secret("token")
implicit val recordingMockBackend: RecordingSttpBackend[IO, Any] = new RecordingSttpBackend(
AsyncHttpClientCatsBackend
Expand All @@ -96,11 +103,12 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
response2.code shouldBe StatusCode.Ok
recordingMockBackend.invocationCountByUri shouldBe Map(tokenUrl -> 1, testAppUrl -> 2)
}
}.unsafeToFuture()
}
}

"TestApp is invoked after token expires" should {
"first invocation is requesting a token, second invocation is requesting a token, because the previous token is expired. add the token to the both TestApp requests" in {
"first invocation is requesting a token, second invocation is requesting a token, because the previous token is expired. add the token to the both TestApp requests" in ticked {

val accessToken1: Secret[String] = Secret("token1")
val accessToken2: Secret[String] = Secret("token2")
implicit val recordingMockBackend: RecordingSttpBackend[IO, Any] = new RecordingSttpBackend(
Expand Down Expand Up @@ -130,7 +138,7 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
response2.body shouldBe "body2"
recordingMockBackend.invocationCountByUri shouldBe Map(tokenUrl -> 2, testAppUrl -> 2)
}
}.unsafeToFuture()
}
}

implicit class SttpBackendStubOps[F[_], P](val backend: SttpBackendStub[F, P]) {
Expand Down