Skip to content

Commit

Permalink
refactor: Use sttpbackend and reuse access token if not expired (#2968)
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Dec 19, 2023
1 parent 95ad2f9 commit eac470a
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
package org.knora.webapi.slice.admin.domain.service

import sttp.capabilities.zio.ZioStreams
import sttp.client3.SttpBackend
import sttp.client3.UriContext
import sttp.client3.asStreamAlways
import sttp.client3.basicRequest
import sttp.client3.httpclient.zio.HttpClientZioBackend
import zio.Clock
import zio.Ref
import zio.Scope
import zio.Task
import zio.UIO
import zio.ZIO
import zio.ZLayer
import zio.http.Body
Expand All @@ -26,9 +30,11 @@ import zio.nio.file.Files
import zio.nio.file.Path
import zio.stream.ZSink

import java.util.concurrent.TimeUnit
import scala.concurrent.duration.DurationInt

import org.knora.webapi.config.DspIngestConfig
import org.knora.webapi.routing.Jwt
import org.knora.webapi.routing.JwtService
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode

Expand All @@ -38,27 +44,40 @@ trait DspIngestClient {

def importProject(shortcode: Shortcode, fileToImport: Path): Task[Path]
}

final case class DspIngestClientLive(
jwtService: JwtService,
dspIngestConfig: DspIngestConfig
dspIngestConfig: DspIngestConfig,
sttpBackend: SttpBackend[Task, ZioStreams],
tokenRef: Ref[Option[Jwt]]
) extends DspIngestClient {

private def projectsPath(shortcode: Shortcode) = s"${dspIngestConfig.baseUrl}/projects/${shortcode.value}"

private val getJwtString: UIO[String] = for {
// check the current token and create a new one if:
// * it is not present
// * it is expired or close to expiring within the next 10 seconds
threshold <- Clock.currentTime(TimeUnit.SECONDS).map(_ - 10)
token <- tokenRef.get.flatMap {
case Some(jwt) if jwt.expiration <= threshold => ZIO.succeed(jwt)
case _ => jwtService.createJwtForDspIngest().tap(jwt => tokenRef.set(Some(jwt)))
}
} yield token.jwtString

private val authenticatedRequest = getJwtString.map(basicRequest.auth.bearer(_))

def exportProject(shortcode: Shortcode): ZIO[Scope, Throwable, Path] =
for {
token <- jwtService.createJwtForDspIngest()
tempdir <- Files.createTempDirectoryScoped(Some("export"), List.empty)
exportFile = tempdir / "export.zip"
response <- {
val request = basicRequest.auth
.bearer(token.jwtString)
.post(uri"${projectsPath(shortcode)}/export")
.readTimeout(30.minutes)
.response(asStreamAlways(ZioStreams)(_.run(ZSink.fromFile(exportFile.toFile))))
HttpClientZioBackend.scoped().flatMap(request.send(_))
}
_ <- ZIO.logInfo(s"Response from ingest :${response.code}")
tempDir <- Files.createTempDirectoryScoped(Some("export"), List.empty)
exportFile = tempDir / "export.zip"
request <- authenticatedRequest.map {
_.post(uri"${projectsPath(shortcode)}/export")
.readTimeout(30.minutes)
.response(asStreamAlways(ZioStreams)(_.run(ZSink.fromFile(exportFile.toFile))))
}
response <- request.send(backend = sttpBackend)
_ <- ZIO.logInfo(s"Response from ingest :${response.code}")
} yield exportFile

def importProject(shortcode: Shortcode, fileToImport: Path): Task[Path] = ZIO.scoped {
Expand All @@ -81,5 +100,8 @@ final case class DspIngestClientLive(
}

object DspIngestClientLive {
val layer = ZLayer.fromFunction(DspIngestClientLive.apply _)
val layer =
HttpClientZioBackend.layer().orDie >+>
ZLayer.fromZIO(Ref.make[Option[Jwt]](None)) >>>
ZLayer.derive[DspIngestClientLive]
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,33 +43,40 @@ object DspIngestClientLiveSpec extends ZIOSpecDefault {
private val testProject = Shortcode.unsafeFrom(testShortCodeStr)
private val testContent = "testContent".getBytes()
private val expectedPath = s"/projects/$testShortCodeStr/export"

private val exportProjectSuite = suite("exportProject")(test("should download a project export") {
ZIO.scoped {
for {
// given
wiremock <- ZIO.service[WireMockServer]
_ = wiremock.stubFor(
WireMock
.post(urlPathEqualTo(expectedPath))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/zip")
.withHeader("Content-Disposition", s"export-$testShortCodeStr.zip")
.withBody(testContent)
.withStatus(200)
)
)
mockJwt <- JwtService.createJwtForDspIngest()

// when
path <- DspIngestClient.exportProject(testProject)

// then
_ = wiremock.verify(
postRequestedFor(urlPathEqualTo(expectedPath))
.withHeader("Authorization", equalTo(s"Bearer ${mockJwt.jwtString}"))
)
contentIsDownloaded <- Files.readAllBytes(path).map(_.toArray).map(_ sameElements testContent)
} yield assertTrue(contentIsDownloaded)
}
})

override def spec: Spec[TestEnvironment & Scope, Any] =
suite("DspIngestClientLive")(test("should download a project export") {
ZIO.scoped {
for {
wiremock <- ZIO.service[WireMockServer]
_ = wiremock.stubFor(
WireMock
.post(urlPathEqualTo(expectedPath))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/zip")
.withHeader("Content-Disposition", s"export-$testShortCodeStr.zip")
.withBody(testContent)
.withStatus(200)
)
)
path <- DspIngestClient.exportProject(testProject)
contentIsDownloaded <- Files.readAllBytes(path).map(_.toArray).map(_ sameElements testContent)
// Verify the request is valid
mockJwt <- ZIO.serviceWithZIO[JwtService](_.createJwtForDspIngest())
_ = wiremock.verify(
postRequestedFor(urlPathEqualTo(expectedPath))
.withHeader("Authorization", equalTo(s"Bearer ${mockJwt.jwtString}"))
)
} yield assertTrue(contentIsDownloaded)
}
}).provide(
suite("DspIngestClientLive")(exportProjectSuite).provide(
DspIngestClientLive.layer,
dspIngestConfigLayer,
mockJwtServiceLayer,
Expand Down

0 comments on commit eac470a

Please sign in to comment.