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

refactor: Use sttpbackend and reuse access token if not expired #2968

Merged
merged 3 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.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 @@

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 {

Check warning on line 62 in webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala#L62

Usage of get on optional type.
case Some(jwt) if jwt.expiration <= threshold => ZIO.succeed(jwt)

Check warning on line 63 in webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala

View check run for this annotation

Codecov / codecov/patch

webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala#L63

Added line #L63 was not covered by tests
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 @@
}

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