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

Add ForgeSystemAccessTokenRepository implementation #40

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package io.toolsplus.atlassian.connect.play.slick

import io.toolsplus.atlassian.connect.play.api.models.DefaultForgeSystemAccessToken
import org.scalatest.DoNotDiscover
import org.scalatest.concurrent.Eventually
import org.scalatestplus.play.PlaySpec
import play.api.Application
import play.api.test.{DefaultAwaitTimeout, FutureAwaits}

import java.time.Instant
import java.time.temporal.ChronoUnit
import scala.collection.immutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@DoNotDiscover
class SlickForgeSystemAccessTokenRepositoryIt
extends PlaySpec
with PostgresContainerTest
with FutureAwaits
with Eventually
with DefaultAwaitTimeout {

val fakeSystemAccessToken: DefaultForgeSystemAccessToken =
DefaultForgeSystemAccessToken("fake-installation-id",
"fake-api-base-url",
"fake-access-token",
Instant.now())

def fakeSystemAccessTokens(now: Instant): Seq[DefaultForgeSystemAccessToken] =
Seq(
"ari-installation-id-1",
"ari-installation-id-2",
"ari-installation-id-3",
"ari-installation-id-4",
"ari-installation-id-5"
).zip(1 to 5).map {
case (id, index) =>
DefaultForgeSystemAccessToken(id,
s"fake-api-base-url-$index",
s"fake-access-token-$index",
now.plus(index, ChronoUnit.MINUTES))
}

def forgeSystemAccessTokenRepo(
implicit app: Application): SlickForgeSystemAccessTokenRepository =
Application.instanceCache[SlickForgeSystemAccessTokenRepository].apply(app)

"Using a Slick Forge system access token repository" when {

"repository is empty" should {

"not find any hosts when fetching all" in {
await {
forgeSystemAccessTokenRepo.all()
} mustEqual immutable.Seq.empty
}

"return None when trying to find a non existent installation by installation id" in {
await {
forgeSystemAccessTokenRepo.findByInstallationId(
"fake-installation-id")
} mustBe None
}

}

"saving a Forge system access token to the repository" should {

"successfully save the token" in {
withEvolutions {
await {
forgeSystemAccessTokenRepo.save(fakeSystemAccessToken)
} mustEqual fakeSystemAccessToken

await {
forgeSystemAccessTokenRepo.all()
} mustEqual Seq(fakeSystemAccessToken)

await {
forgeSystemAccessTokenRepo.findByInstallationId(
fakeSystemAccessToken.installationId)
} mustBe Some(fakeSystemAccessToken)
}
}

"update a token if the installation id already exists" in {
val fakeTokenWith20sExpiry = fakeSystemAccessToken.copy(
expirationTime = Instant.now().plusSeconds(20))

val fakeTokenWithTwoMinExpiry = fakeSystemAccessToken.copy(
accessToken = "new-fake-access-token",
expirationTime = Instant.now().plus(2, ChronoUnit.MINUTES))

withEvolutions {
await {
forgeSystemAccessTokenRepo.save(fakeTokenWith20sExpiry)
} mustEqual fakeTokenWith20sExpiry

await {
forgeSystemAccessTokenRepo.findByInstallationId(
fakeTokenWith20sExpiry.installationId)
} mustBe Some(fakeTokenWith20sExpiry)

await {
forgeSystemAccessTokenRepo.save(fakeTokenWithTwoMinExpiry)
} mustEqual fakeTokenWithTwoMinExpiry

await {
forgeSystemAccessTokenRepo.findByInstallationId(
fakeTokenWith20sExpiry.installationId)
} mustBe Some(fakeTokenWithTwoMinExpiry)

await {
forgeSystemAccessTokenRepo.all()
} mustEqual Seq(fakeTokenWithTwoMinExpiry)
}
}

"not duplicate the token when saving the same token twice" in {
withEvolutions {
await {
forgeSystemAccessTokenRepo.save(fakeSystemAccessToken)
} mustBe fakeSystemAccessToken

await {
forgeSystemAccessTokenRepo.save(fakeSystemAccessToken)
} mustBe fakeSystemAccessToken

await {
forgeSystemAccessTokenRepo.all()
} mustBe Seq(fakeSystemAccessToken)
}
}

}

"finding tokens by installation id and expiration time after" should {

"find a token after the given expiry time" in {
val fakeTokenWithTwoMinExpiry = fakeSystemAccessToken.copy(
expirationTime = Instant.now().plus(2, ChronoUnit.MINUTES))

val leeway30s = Instant.now().plusSeconds(30)

withEvolutions {
await {
forgeSystemAccessTokenRepo.save(fakeTokenWithTwoMinExpiry)
}

await {
forgeSystemAccessTokenRepo
.findByInstallationIdAndExpirationTimeAfter(
fakeSystemAccessToken.installationId,
leeway30s)
} mustBe Some(fakeTokenWithTwoMinExpiry)
}
}

"return a token if the given expiration time equals the token expiry" in {
val fakeTokenWithTwoMinExpiry = fakeSystemAccessToken.copy(
expirationTime = Instant.now().plus(2, ChronoUnit.MINUTES))

withEvolutions {
await {
forgeSystemAccessTokenRepo.save(fakeTokenWithTwoMinExpiry)
}

await {
forgeSystemAccessTokenRepo
.findByInstallationIdAndExpirationTimeAfter(
fakeSystemAccessToken.installationId,
fakeTokenWithTwoMinExpiry.expirationTime)
} mustBe Some(fakeTokenWithTwoMinExpiry)

await {
forgeSystemAccessTokenRepo
.findByInstallationIdAndExpirationTimeAfter(
fakeSystemAccessToken.installationId,
fakeTokenWithTwoMinExpiry.expirationTime.plusMillis(1))
} mustBe None
}
}

"not return a token if the given expiration time is after the token expiry" in {
val fakeTokenWithTwoMinExpiry = fakeSystemAccessToken.copy(
expirationTime = Instant.now().plus(2, ChronoUnit.MINUTES))

withEvolutions {
await {
forgeSystemAccessTokenRepo.save(fakeTokenWithTwoMinExpiry)
}

await {
forgeSystemAccessTokenRepo
.findByInstallationIdAndExpirationTimeAfter(
fakeSystemAccessToken.installationId,
fakeTokenWithTwoMinExpiry.expirationTime.plusSeconds(60))
} mustBe None

await {
forgeSystemAccessTokenRepo
.all()
} mustBe Seq(fakeTokenWithTwoMinExpiry)
}
}

}

"deleting all tokens with expiration time before" should {
val now = Instant.now()
val tokens = fakeSystemAccessTokens(now)
val nowPlusTwoMin30s = now.plusSeconds(150)

"clean up the repository" in {
withEvolutions {
await {
Future.sequence(tokens.map(forgeSystemAccessTokenRepo.save))
} mustEqual tokens

await {
forgeSystemAccessTokenRepo
.all()
} mustBe tokens

val nRemovedTokens = await(
forgeSystemAccessTokenRepo.deleteAllByExpirationTimeBefore(
nowPlusTwoMin30s))

nRemovedTokens mustEqual 2

await {
forgeSystemAccessTokenRepo
.all()
} mustBe tokens.drop(2)
}
}

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ import org.scalatest.Suites
*/
class SlickRepositoryIt
extends Suites(new SlickAtlassianHostRepositoryIt,
new SlickForgeInstallationRepositoryIt)
new SlickForgeInstallationRepositoryIt,
new SlickForgeSystemAccessTokenRepositoryIt)
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ object Dependencies {
}

object Version {
val atlassianConnectPlay = "0.8.0"
val atlassianConnectPlay = "0.8.1-SNAPSHOT"
val playSlick = "6.1.0"
val scalaTestPlusPlay = "7.0.1"
val scalaCheck = "1.18.0"
Expand Down
11 changes: 11 additions & 0 deletions src/main/resources/evolutions/default/1.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ CREATE UNIQUE INDEX uq_forge_installation_installation_id
ALTER TABLE forge_installation
ADD CONSTRAINT fk_forge_installation_atlassian_host FOREIGN KEY (client_key) REFERENCES atlassian_host (client_key) ON UPDATE CASCADE ON DELETE CASCADE;

CREATE TABLE forge_system_access_token
(
installation_id VARCHAR PRIMARY KEY,
api_base_url VARCHAR NOT NULL,
access_token VARCHAR NOT NULL,
expiration_time TIMESTAMP NOT NULL
);
CREATE INDEX forge_system_access_token_expiration_time
ON forge_system_access_token (expiration_time);

# --- !Downs
DROP TABLE forge_system_access_token;
DROP TABLE forge_installation;
DROP TABLE atlassian_host;
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.toolsplus.atlassian.connect.play.slick

import io.toolsplus.atlassian.connect.play.api.models.{
DefaultForgeSystemAccessToken,
ForgeSystemAccessToken
}
import io.toolsplus.atlassian.connect.play.api.repositories.ForgeSystemAccessTokenRepository
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import slick.jdbc.JdbcProfile
import slick.sql.SqlProfile.ColumnOption.NotNull

import java.time.Instant
import javax.inject.{Inject, Singleton}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@Singleton
class SlickForgeSystemAccessTokenRepository @Inject()(
protected val dbConfigProvider: DatabaseConfigProvider)
extends ForgeSystemAccessTokenRepository
with ForgeSystemAccessTokenTable
with HasDatabaseConfigProvider[JdbcProfile] {

import profile.api._

override def all(): Future[Seq[ForgeSystemAccessToken]] =
db.run(systemAccessTokens.result)

override def save(
token: ForgeSystemAccessToken): Future[ForgeSystemAccessToken] =
db.run(systemAccessTokens.insertOrUpdate(token)).map(_ => token)

override def findByInstallationId(
installationId: String): Future[Option[ForgeSystemAccessToken]] =
db.run(
systemAccessTokens
.filter(t => t.installationId === installationId)
.result
.headOption
)

override def findByInstallationIdAndExpirationTimeAfter(
installationId: String,
expirationTime: Instant): Future[Option[ForgeSystemAccessToken]] =
db.run(
systemAccessTokens
.filter(t =>
t.installationId === installationId && t.expirationTime >= expirationTime)
.result
.headOption
)

override def deleteAllByExpirationTimeBefore(
expirationTime: Instant): Future[Int] =
db.run(systemAccessTokens.filter(_.expirationTime < expirationTime).delete)
}

private[slick] trait ForgeSystemAccessTokenTable {

self: HasDatabaseConfigProvider[JdbcProfile] =>

import profile.api._

lazy protected val systemAccessTokens = TableQuery[Schema]

private[ForgeSystemAccessTokenTable] class Schema(tag: Tag)
extends Table[ForgeSystemAccessToken](tag, "forge_system_access_token") {
val installationId = column[String]("installation_id", O.PrimaryKey)
val apiBaseUrl = column[String]("api_base_url", NotNull)
val accessToken = column[String]("access_token", NotNull)
val expirationTime = column[Instant]("expiration_time", NotNull)

val expirationTimeIndex =
index("forge_system_access_token_expiration_time", expirationTime)

def * =
(installationId, apiBaseUrl, accessToken, expirationTime) <> (toSystemAccessToken.tupled, fromSystemAccessToken)

private def toSystemAccessToken
: (String, String, String, Instant) => ForgeSystemAccessToken =
DefaultForgeSystemAccessToken.apply
}

private def fromSystemAccessToken
: ForgeSystemAccessToken => Option[(String, String, String, Instant)] = {
systemAccessToken: ForgeSystemAccessToken =>
DefaultForgeSystemAccessToken.unapply(
DefaultForgeSystemAccessToken(
systemAccessToken.installationId,
systemAccessToken.apiBaseUrl,
systemAccessToken.accessToken,
systemAccessToken.expirationTime
))
}

}
Loading
Loading