diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala index cb18337892..d2c1180b6a 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala @@ -36,7 +36,6 @@ import org.scalasteward.core.edit.EditAlg import org.scalasteward.core.edit.hooks.HookExecutor import org.scalasteward.core.edit.scalafix._ import org.scalasteward.core.edit.update.ScannerAlg -import org.scalasteward.core.forge.github.{GitHubAppApiAlg, GitHubAuthAlg} import org.scalasteward.core.forge.{ForgeApiAlg, ForgeAuthAlg, ForgeRepoAlg, ForgeSelection} import org.scalasteward.core.git.{GenGitAlg, GitAlg} import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} @@ -128,18 +127,18 @@ object Context { processAlg: ProcessAlg[F], workspaceAlg: WorkspaceAlg[F], F: Async[F] - ): F[Context[F]] = + ): F[Context[F]] = { + implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F]() + implicit val forgeAuthAlg: ForgeAuthAlg[F] = ForgeAuthAlg.create[F](config) for { _ <- F.unit - forgeUser = new ForgeAuthAlg[F](config.gitCfg, config.forgeCfg).forgeUser artifactMigrationsLoader0 = new ArtifactMigrationsLoader[F] artifactMigrationsFinder0 <- artifactMigrationsLoader0.createFinder(config.artifactCfg) scalafixMigrationsLoader0 = new ScalafixMigrationsLoader[F] scalafixMigrationsFinder0 <- scalafixMigrationsLoader0.createFinder(config.scalafixCfg) repoConfigLoader0 = new RepoConfigLoader[F] maybeGlobalRepoConfig <- repoConfigLoader0.loadGlobalRepoConfig(config.repoConfigCfg) - urlChecker0 <- UrlChecker - .create[F](config, ForgeSelection.authenticateIfApiHost(config.forgeCfg, forgeUser)) + urlChecker0 <- UrlChecker.create[F](config, forgeAuthAlg.authenticateApi) kvsPrefix = Some(config.forgeCfg.tpe.asString) pullRequestsStore <- JsonKeyValueStore .create[F, Repo, Map[Uri, PullRequestRepository.Entry]]("pull_requests", "2", kvsPrefix) @@ -159,14 +158,12 @@ object Context { implicit val dateTimeAlg: DateTimeAlg[F] = DateTimeAlg.create[F] implicit val repoConfigAlg: RepoConfigAlg[F] = new RepoConfigAlg[F](maybeGlobalRepoConfig) implicit val filterAlg: FilterAlg[F] = new FilterAlg[F] - implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config.gitCfg) - implicit val gitHubAuthAlg: GitHubAuthAlg[F] = GitHubAuthAlg.create[F] + implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config) implicit val hookExecutor: HookExecutor[F] = new HookExecutor[F] - implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F] implicit val repoCacheRepository: RepoCacheRepository[F] = new RepoCacheRepository[F](repoCacheStore) - implicit val forgeApiAlg: ForgeApiAlg[F] = - ForgeSelection.forgeApiAlg[F](config.forgeCfg, config.forgeSpecificCfg, forgeUser) + implicit val forgeApiAlg: ForgeApiAlg[F] = ForgeSelection + .forgeApiAlg[F](config.forgeCfg, config.forgeSpecificCfg, forgeAuthAlg.authenticateApi) implicit val forgeRepoAlg: ForgeRepoAlg[F] = new ForgeRepoAlg[F](config) implicit val forgeCfg: ForgeCfg = config.forgeCfg implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F] @@ -192,11 +189,10 @@ object Context { implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config.forgeCfg) implicit val pruningAlg: PruningAlg[F] = new PruningAlg[F] implicit val reposFilesLoader: ReposFilesLoader[F] = new ReposFilesLoader[F] - implicit val gitHubAppApiAlg: GitHubAppApiAlg[F] = - new GitHubAppApiAlg[F](config.forgeCfg.apiHost) implicit val stewardAlg: StewardAlg[F] = new StewardAlg[F](config) new Context[F] } + } private val banner: String = { val banner = diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala index afbe9633d9..42b00015a6 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala @@ -20,7 +20,7 @@ import cats.effect.{ExitCode, Sync} import cats.syntax.all._ import fs2.Stream import org.scalasteward.core.data.Repo -import org.scalasteward.core.forge.github.{GitHubApp, GitHubAppApiAlg, GitHubAuthAlg} +import org.scalasteward.core.forge.ForgeAuthAlg import org.scalasteward.core.git.GitAlg import org.scalasteward.core.io.{FileAlg, WorkspaceAlg} import org.scalasteward.core.nurture.NurtureAlg @@ -30,14 +30,12 @@ import org.scalasteward.core.util import org.scalasteward.core.util.DateTimeAlg import org.scalasteward.core.util.logger.LoggerOps import org.typelevel.log4cats.Logger -import scala.concurrent.duration._ final class StewardAlg[F[_]](config: Config)(implicit dateTimeAlg: DateTimeAlg[F], fileAlg: FileAlg[F], gitAlg: GitAlg[F], - githubAppApiAlg: GitHubAppApiAlg[F], - githubAuthAlg: GitHubAuthAlg[F], + forgeAuthAlg: ForgeAuthAlg[F], logger: Logger[F], nurtureAlg: NurtureAlg[F], pruningAlg: PruningAlg[F], @@ -47,25 +45,6 @@ final class StewardAlg[F[_]](config: Config)(implicit workspaceAlg: WorkspaceAlg[F], F: Sync[F] ) { - private def getGitHubAppRepos(githubApp: GitHubApp): Stream[F, Repo] = - Stream.evals[F, List, Repo] { - for { - jwt <- githubAuthAlg.createJWT(githubApp, 2.minutes) - installations <- githubAppApiAlg.installations(jwt) - repositories <- installations.traverse { installation => - githubAppApiAlg - .accessToken(jwt, installation.id) - .flatMap(token => githubAppApiAlg.repositories(token.token)) - } - repos <- repositories.flatMap(_.repositories).flatTraverse { repo => - repo.full_name.split('/') match { - case Array(owner, name) => F.pure(List(Repo(owner, name))) - case _ => logger.error(s"invalid repo $repo").as(List.empty[Repo]) - } - } - } yield repos - } - private def steward(repo: Repo): F[Either[Throwable, Unit]] = { val label = s"Steward ${repo.show}" logger.infoTotalTime(label) { @@ -88,7 +67,7 @@ final class StewardAlg[F[_]](config: Config)(implicit _ <- selfCheckAlg.checkAll _ <- workspaceAlg.removeAnyRunSpecificFiles exitCode <- - (config.githubApp.map(getGitHubAppRepos).getOrElse(Stream.empty) ++ + (Stream.evals(forgeAuthAlg.accessibleRepos) ++ reposFilesLoader.loadAll(config.reposFiles)) .evalMap(repo => steward(repo).map(_.bimap(repo -> _, _ => repo))) .compile diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/BasicAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/BasicAuthAlg.scala new file mode 100644 index 0000000000..eb7eea5793 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/BasicAuthAlg.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2018-2023 Scala Steward contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.scalasteward.core.forge + +import better.files.File +import cats.effect.Sync +import cats.syntax.all._ +import org.http4s.Uri.UserInfo +import org.http4s.headers.Authorization +import org.http4s.{BasicCredentials, Request, Uri} +import org.scalasteward.core.data.Repo +import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg} +import org.scalasteward.core.util +import org.scalasteward.core.util.Nel + +class BasicAuthAlg[F[_]](apiUri: Uri, login: String, gitAskPass: File)(implicit + F: Sync[F], + workspaceAlg: WorkspaceAlg[F], + processAlg: ProcessAlg[F] +) extends ForgeAuthAlg[F] { + protected lazy val userInfo: F[UserInfo] = for { + rootDir <- workspaceAlg.rootDir + userInfo = UserInfo(login, None) + urlWithUser = util.uri.withUserInfo.replace(userInfo)(apiUri).renderString + prompt = s"Password for '$urlWithUser': " + output <- processAlg.exec(Nel.of(gitAskPass.pathAsString, prompt), rootDir) + password = output.mkString.trim + } yield UserInfo(login, Some(password)) + + override def authenticateApi(req: Request[F]): F[Request[F]] = + userInfo.map { + case UserInfo(username, Some(password)) => + req.putHeaders(Authorization(BasicCredentials(username, password))) + case _ => req + } + + override def authenticateGit(uri: Uri): F[Uri] = + userInfo.map(user => util.uri.withUserInfo.replace(user)(uri)) + + override def accessibleRepos: F[List[Repo]] = F.pure(List.empty) +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala index 65debaa02e..ac851d9b35 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala @@ -16,27 +16,52 @@ package org.scalasteward.core.forge -import cats.Monad -import cats.syntax.all._ -import org.http4s.Uri.UserInfo -import org.scalasteward.core.application.Config.{ForgeCfg, GitCfg} -import org.scalasteward.core.forge.data.AuthenticatedUser +import cats.effect.Sync +import org.http4s.{Request, Uri} +import org.scalasteward.core.application.Config +import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.ForgeType._ +import org.scalasteward.core.forge.bitbucketserver.BitbucketServerAuthAlg +import org.scalasteward.core.forge.github.GitHubAuthAlg +import org.scalasteward.core.forge.gitlab.GitLabAuthAlg import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg} -import org.scalasteward.core.util -import org.scalasteward.core.util.Nel +import org.scalasteward.core.util.HttpJsonClient +import org.typelevel.log4cats.Logger -final class ForgeAuthAlg[F[_]](gitCfg: GitCfg, forgeCfg: ForgeCfg)(implicit - processAlg: ProcessAlg[F], - workspaceAlg: WorkspaceAlg[F], - F: Monad[F] -) { - def forgeUser: F[AuthenticatedUser] = - for { - rootDir <- workspaceAlg.rootDir - userInfo = UserInfo(forgeCfg.login, None) - urlWithUser = util.uri.withUserInfo.replace(userInfo)(forgeCfg.apiHost).renderString - prompt = s"Password for '$urlWithUser': " - output <- processAlg.exec(Nel.of(gitCfg.gitAskPass.pathAsString, prompt), rootDir) - password = output.mkString.trim - } yield AuthenticatedUser(forgeCfg.login, password) +trait ForgeAuthAlg[F[_]] { + def authenticateApi(req: Request[F]): F[Request[F]] + def authenticateGit(uri: Uri): F[Uri] + def accessibleRepos: F[List[Repo]] +} + +object ForgeAuthAlg { + def create[F[_]](config: Config)(implicit + F: Sync[F], + client: HttpJsonClient[F], + workspaceAlg: WorkspaceAlg[F], + processAlg: ProcessAlg[F], + logger: Logger[F] + ): ForgeAuthAlg[F] = + config.forgeCfg.tpe match { + case AzureRepos => + new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass) + case Bitbucket => + new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass) + case BitbucketServer => + new BitbucketServerAuthAlg( + config.forgeCfg.apiHost, + config.forgeCfg.login, + config.gitCfg.gitAskPass + ) + case GitHub => + val gitHub = + config.githubApp.getOrElse( + throw new IllegalArgumentException("GitHub app configuration is missing") + ) + new GitHubAuthAlg(config.forgeCfg.apiHost, gitHub.id, gitHub.keyFile) + case GitLab => + new GitLabAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass) + case Gitea => + new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass) + } } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepoAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepoAlg.scala index 150883dbc4..aa6503e251 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepoAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepoAlg.scala @@ -18,29 +18,29 @@ package org.scalasteward.core.forge import cats.MonadThrow import cats.syntax.all._ -import org.http4s.Uri -import org.http4s.Uri.UserInfo import org.scalasteward.core.application.Config import org.scalasteward.core.data.Repo import org.scalasteward.core.forge.ForgeType.GitHub import org.scalasteward.core.forge.data.RepoOut import org.scalasteward.core.git.{updateBranchPrefix, Branch, GitAlg} -import org.scalasteward.core.util import org.scalasteward.core.util.logger._ import org.typelevel.log4cats.Logger final class ForgeRepoAlg[F[_]](config: Config)(implicit gitAlg: GitAlg[F], + forgeAuthAlg: ForgeAuthAlg[F], logger: Logger[F], F: MonadThrow[F] ) { def cloneAndSync(repo: Repo, repoOut: RepoOut): F[Unit] = clone(repo, repoOut) >> maybeCheckoutBranchOrSyncFork(repo, repoOut) >> initSubmodules(repo) - private def clone(repo: Repo, repoOut: RepoOut): F[Unit] = - logger.info(s"Clone ${repoOut.repo.show}") >> - gitAlg.clone(repo, withLogin(repoOut.clone_url)).adaptErr(adaptCloneError) >> - gitAlg.setAuthor(repo, config.gitCfg.gitAuthor) + private def clone(repo: Repo, repoOut: RepoOut): F[Unit] = for { + _ <- logger.info(s"Clone ${repoOut.repo.show}") + uri <- forgeAuthAlg.authenticateGit(repoOut.clone_url) + _ <- gitAlg.clone(repo, uri).adaptErr(adaptCloneError) + _ <- gitAlg.setAuthor(repo, config.gitCfg.gitAuthor) + } yield () private val adaptCloneError: PartialFunction[Throwable, Throwable] = { case throwable if config.forgeCfg.tpe === GitHub && !config.forgeCfg.doNotFork => @@ -56,12 +56,13 @@ final class ForgeRepoAlg[F[_]](config: Config)(implicit if (config.forgeCfg.doNotFork) repo.branch.fold(F.unit)(gitAlg.checkoutBranch(repo, _)) else syncFork(repo, repoOut) - private def syncFork(repo: Repo, repoOut: RepoOut): F[Unit] = - repoOut.parentOrRaise[F].flatMap { parent => - logger.info(s"Synchronize with ${parent.repo.show}") >> - gitAlg.syncFork(repo, withLogin(parent.clone_url), parent.default_branch) >> - deleteUpdateBranch(repo) - } + private def syncFork(repo: Repo, repoOut: RepoOut): F[Unit] = for { + parent <- repoOut.parentOrRaise[F] + _ <- logger.info(s"Synchronize with ${parent.repo.show}") + uri <- forgeAuthAlg.authenticateGit(parent.clone_url) + _ <- gitAlg.syncFork(repo, uri, parent.default_branch) + _ <- deleteUpdateBranch(repo) + } yield () // We use "update" as prefix for our branches but Git doesn't allow branches named // "update" and "update/..." in the same repo. We therefore delete the "update" branch @@ -77,7 +78,4 @@ final class ForgeRepoAlg[F[_]](config: Config)(implicit logger.attemptWarn.log_("Initializing and cloning submodules failed") { gitAlg.initSubmodules(repo) } - - private val withLogin: Uri => Uri = - util.uri.withUserInfo.replace(UserInfo(config.forgeCfg.login, None)) } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala index 9fb7c0ca1f..d797322aa1 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala @@ -17,35 +17,29 @@ package org.scalasteward.core.forge import cats.effect.Temporal -import cats.syntax.all._ -import cats.{Applicative, Functor, Parallel} -import org.http4s.headers.Authorization -import org.http4s.{BasicCredentials, Header, Request} +import cats.Parallel +import org.http4s.Request import org.scalasteward.core.application.Config import org.scalasteward.core.application.Config.{ForgeCfg, ForgeSpecificCfg} -import org.scalasteward.core.forge.ForgeType._ import org.scalasteward.core.forge.azurerepos.AzureReposApiAlg import org.scalasteward.core.forge.bitbucket.BitbucketApiAlg import org.scalasteward.core.forge.bitbucketserver.BitbucketServerApiAlg -import org.scalasteward.core.forge.data.AuthenticatedUser import org.scalasteward.core.forge.gitea.GiteaApiAlg import org.scalasteward.core.forge.github.GitHubApiAlg import org.scalasteward.core.forge.gitlab.GitLabApiAlg import org.scalasteward.core.util.HttpJsonClient -import org.typelevel.ci._ import org.typelevel.log4cats.Logger object ForgeSelection { def forgeApiAlg[F[_]: Parallel]( forgeCfg: ForgeCfg, forgeSpecificCfg: ForgeSpecificCfg, - user: F[AuthenticatedUser] + auth: Request[F] => F[Request[F]] )(implicit httpJsonClient: HttpJsonClient[F], logger: Logger[F], F: Temporal[F] - ): ForgeApiAlg[F] = { - val auth = authenticate(forgeCfg.tpe, user) + ): ForgeApiAlg[F] = forgeSpecificCfg match { case specificCfg: Config.AzureReposCfg => new AzureReposApiAlg(forgeCfg.apiHost, specificCfg, auth) @@ -60,39 +54,4 @@ object ForgeSelection { case _: Config.GiteaCfg => new GiteaApiAlg(forgeCfg, auth) } - } - - def authenticate[F[_]]( - forgeType: ForgeType, - user: F[AuthenticatedUser] - )(implicit F: Functor[F]): Request[F] => F[Request[F]] = - forgeType match { - case AzureRepos => req => user.map(u => req.putHeaders(basicAuth(u))) - case Bitbucket => req => user.map(u => req.putHeaders(basicAuth(u))) - case BitbucketServer => req => user.map(u => req.putHeaders(basicAuth(u), xAtlassianToken)) - case GitHub => req => user.map(u => req.putHeaders(basicAuth(u))) - case GitLab => req => user.map(u => req.putHeaders(privateToken(u))) - case Gitea => req => user.map(u => req.putHeaders(basicAuth(u))) - } - - private def basicAuth(user: AuthenticatedUser): Authorization = - Authorization(BasicCredentials(user.login, user.accessToken)) - - private def privateToken(user: AuthenticatedUser): Header.Raw = - Header.Raw(ci"Private-Token", user.accessToken) - - // Bypass the server-side XSRF check, see - // https://github.com/scala-steward-org/scala-steward/pull/1863#issuecomment-754538364 - private val xAtlassianToken = Header.Raw(ci"X-Atlassian-Token", "no-check") - - def authenticateIfApiHost[F[_]]( - forgeCfg: ForgeCfg, - user: F[AuthenticatedUser] - )(implicit F: Applicative[F]): Request[F] => F[Request[F]] = - req => { - val sameScheme = req.uri.scheme === forgeCfg.apiHost.scheme - val sameHost = req.uri.host === forgeCfg.apiHost.host - if (sameScheme && sameHost) authenticate(forgeCfg.tpe, user)(F)(req) - else req.pure[F] - } } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerAuthAlg.scala new file mode 100644 index 0000000000..e3c8ee8137 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerAuthAlg.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2018-2023 Scala Steward contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.scalasteward.core.forge.bitbucketserver + +import better.files.File +import cats.effect.Sync +import cats.syntax.all._ +import org.http4s.Uri.UserInfo +import org.http4s.headers.Authorization +import org.http4s.{BasicCredentials, Header, Request, Uri} +import org.scalasteward.core.forge.BasicAuthAlg +import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg} +import org.typelevel.ci.CIStringSyntax + +class BitbucketServerAuthAlg[F[_]](apiUri: Uri, login: String, gitAskPass: File)(implicit + F: Sync[F], + workspaceAlg: WorkspaceAlg[F], + processAlg: ProcessAlg[F] +) extends BasicAuthAlg[F](apiUri, login, gitAskPass) { + override def authenticateApi(req: Request[F]): F[Request[F]] = + userInfo.map { + case UserInfo(username, Some(password)) => + req.putHeaders( + Authorization(BasicCredentials(username, password)), + // Bypass the server-side XSRF check, see + // https://github.com/scala-steward-org/scala-steward/pull/1863#issuecomment-754538364 + Header.Raw(ci"X-Atlassian-Token", "no-check") + ) + case _ => req + } +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala index be9abd615d..058147975c 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala @@ -17,13 +17,14 @@ package org.scalasteward.core.forge.data import cats.ApplicativeThrow -import io.circe.Decoder +import io.circe.Codec import io.circe.generic.semiauto._ import org.http4s.Uri import org.scalasteward.core.data.Repo import org.scalasteward.core.git.Branch import org.scalasteward.core.util.intellijThisImportIsUsed import org.scalasteward.core.util.uri.uriDecoder +import org.scalasteward.core.util.uri.uriEncoder final case class RepoOut( name: String, @@ -45,8 +46,7 @@ final case class RepoOut( } object RepoOut { - implicit val repoOutDecoder: Decoder[RepoOut] = - deriveDecoder + implicit val repoOutDecoder: Codec[RepoOut] = deriveCodec intellijThisImportIsUsed(uriDecoder) } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/data/UserOut.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/data/UserOut.scala index 6149dafb6e..843aa94720 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/data/UserOut.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/data/UserOut.scala @@ -16,7 +16,7 @@ package org.scalasteward.core.forge.data -import io.circe.Decoder +import io.circe.Codec import io.circe.generic.semiauto._ final case class UserOut( @@ -24,6 +24,5 @@ final case class UserOut( ) object UserOut { - implicit val userOutDecoder: Decoder[UserOut] = - deriveDecoder + implicit val userOutDecoder: Codec[UserOut] = deriveCodec } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala index 8ffbb10f37..71894276da 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala @@ -26,60 +26,136 @@ import java.security.{KeyFactory, PrivateKey, Security} import java.util.Date import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.util.io.pem.PemReader -import scala.concurrent.duration.FiniteDuration +import org.http4s.Credentials.Token +import org.http4s.Uri.UserInfo +import org.http4s.headers.Authorization +import org.http4s.{AuthScheme, Header, Request, Uri} +import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.ForgeAuthAlg +import org.scalasteward.core.util.HttpJsonClient +import org.typelevel.ci.CIStringSyntax +import org.typelevel.log4cats.Logger +import scala.concurrent.duration.{DurationInt, FiniteDuration} import scala.util.Using +import org.scalasteward.core.util -trait GitHubAuthAlg[F[_]] { +final class GitHubAuthAlg[F[_]]( + apiUri: Uri, + appId: Long, + appKeyFile: File +)(implicit F: Sync[F], client: HttpJsonClient[F], logger: Logger[F]) + extends ForgeAuthAlg[F] { + private val tokenTtl = 2.minutes - /** [[https://docs.github.com/en/free-pro-team@latest/developers/apps/authenticating-with-github-apps#authenticating-as-a-github-app]] + private def parsePEMFile(pemFile: File): Array[Byte] = + Using.resource(new PemReader(new FileReader(pemFile.toJava))) { reader => + reader.readPemObject().getContent + } + + private def getPrivateKey(keyBytes: Array[Byte]): PrivateKey = { + val kf = KeyFactory.getInstance("RSA") + val keySpec = new PKCS8EncodedKeySpec(keyBytes) + kf.generatePrivate(keySpec) + } + + private def readPrivateKey(appKeyFile: File): PrivateKey = { + val bytes = parsePEMFile(appKeyFile) + getPrivateKey(bytes) + } + + override def authenticateApi(req: Request[F]): F[Request[F]] = for { + tokenRepos <- tokenRepos + maybeToken = tokenRepos + .find(_._1.exists(repo => req.uri.toString.contains(repo.toPath))) + .map(_._2.token) + } yield maybeToken match { + case Some(token) => req.putHeaders(Authorization(Token(AuthScheme.Bearer, token))) + case None => req + } + + override def authenticateGit(uri: Uri): F[Uri] = for { + tokenRepos <- tokenRepos + tokenMaybe = tokenRepos + .find(_._1.exists(repo => uri.toString.contains(repo.toPath))) + .map(_._2.token) + } yield util.uri.withUserInfo.replace(UserInfo("scala-steward", tokenMaybe))(uri) + + private def tokenRepos = for { + jwt <- createJWT(tokenTtl) + installations <- installations(jwt) + tokens <- installations.traverse(installation => accessToken(jwt, installation.id)) + tokenRepos <- tokens.traverse(token => repositories(token.token).map(_ -> token)) + } yield tokenRepos + + override def accessibleRepos: F[List[Repo]] = for { + jwt <- createJWT(tokenTtl) + installations <- installations(jwt) + tokens <- installations.traverse(installation => accessToken(jwt, installation.id)) + repos <- tokens.flatTraverse(token => repositories(token.token)) + } yield repos + + /** [[https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-repositories-accessible-to-the-app-installation]] */ - def createJWT(app: GitHubApp, ttl: FiniteDuration): F[String] + private[github] def repositories(token: String): F[List[Repo]] = + client + .getAll[RepositoriesOut]( + (apiUri / "installation" / "repositories").withQueryParam("per_page", 100), + req => F.point(req.putHeaders(Header.Raw(ci"Authorization", s"token $token"))) + ) + .compile + .toList + .flatMap(values => + values + .flatMap(_.repositories) + .flatTraverse(_.full_name.split('/') match { + case Array(owner, name) => F.pure(List(Repo(owner, name))) + case _ => logger.error(s"invalid repo ").as(List.empty[Repo]) + }) + ) - def createJWT(app: GitHubApp, ttl: FiniteDuration, nowMillis: Long): F[String] + /** [[https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-installations-for-the-authenticated-app]] + */ + private[github] def installations(jwt: String): F[List[InstallationOut]] = + client + .getAll[List[InstallationOut]]( + (apiUri / "app" / "installations").withQueryParam("per_page", 100), + addHeaders(jwt) + ) + .compile + .foldMonoid -} + /** [[https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#create-an-installation-access-token-for-an-app]] + */ + private[github] def accessToken(jwt: String, installationId: Long): F[TokenOut] = + client.post( + apiUri / "app" / "installations" / installationId.toString / "access_tokens", + addHeaders(jwt) + ) -object GitHubAuthAlg { - def create[F[_]](implicit F: Sync[F]): GitHubAuthAlg[F] = - new GitHubAuthAlg[F] { - private[this] def parsePEMFile(pemFile: File): Array[Byte] = - Using.resource(new PemReader(new FileReader(pemFile.toJava))) { reader => - reader.readPemObject().getContent - } - - private[this] def getPrivateKey(keyBytes: Array[Byte]): PrivateKey = { - val kf = KeyFactory.getInstance("RSA") - val keySpec = new PKCS8EncodedKeySpec(keyBytes) - kf.generatePrivate(keySpec) - } + /** [[https://docs.github.com/en/free-pro-team@latest/developers/apps/authenticating-with-github-apps#authenticating-as-a-github-app]] + */ + private[github] def createJWT(ttl: FiniteDuration): F[String] = + F.delay(System.currentTimeMillis()).flatMap(createJWT(ttl, _)) - private[this] def readPrivateKey(file: File): PrivateKey = { - val bytes = parsePEMFile(file) - getPrivateKey(bytes) + private[github] def createJWT(ttl: FiniteDuration, nowMillis: Long): F[String] = + F.delay { + Security.addProvider(new BouncyCastleProvider()) + val ttlMillis = ttl.toMillis + val now = new Date(nowMillis) + val signingKey = readPrivateKey(appKeyFile) + val builder = Jwts + .builder() + .issuedAt(now) + .issuer(appId.toString) + .signWith(signingKey, Jwts.SIG.RS256) + if (ttlMillis > 0) { + val expMillis = nowMillis + ttlMillis + val exp = new Date(expMillis) + builder.expiration(exp) } - - /** [[https://docs.github.com/en/free-pro-team@latest/developers/apps/authenticating-with-github-apps#authenticating-as-a-github-app]] - */ - def createJWT(app: GitHubApp, ttl: FiniteDuration): F[String] = - F.delay(System.currentTimeMillis()).flatMap(createJWT(app, ttl, _)) - - def createJWT(app: GitHubApp, ttl: FiniteDuration, nowMillis: Long): F[String] = - F.delay { - Security.addProvider(new BouncyCastleProvider()) - val ttlMillis = ttl.toMillis - val now = new Date(nowMillis) - val signingKey = readPrivateKey(app.keyFile) - val builder = Jwts - .builder() - .issuedAt(now) - .issuer(app.id.toString) - .signWith(signingKey, Jwts.SIG.RS256) - if (ttlMillis > 0) { - val expMillis = nowMillis + ttlMillis - val exp = new Date(expMillis) - builder.expiration(exp) - } - builder.compact() - } + builder.compact() } + + private def addHeaders(jwt: String): client.ModReq = + req => F.point(req.putHeaders(Authorization(Token(AuthScheme.Bearer, jwt)))) } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/InstallationOut.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/InstallationOut.scala index dd963959e0..a331998a6f 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/InstallationOut.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/github/InstallationOut.scala @@ -16,10 +16,11 @@ package org.scalasteward.core.forge.github -import io.circe.Decoder -import io.circe.generic.semiauto.deriveDecoder +import io.circe.{Decoder, Encoder} +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} case class InstallationOut(id: Long) object InstallationOut { + implicit val installationEncoder: Encoder[InstallationOut] = deriveEncoder implicit val installationDecoder: Decoder[InstallationOut] = deriveDecoder } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/RepositoriesOut.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/RepositoriesOut.scala index 86c6a6f4f2..bbb555db56 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/RepositoriesOut.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/github/RepositoriesOut.scala @@ -16,15 +16,15 @@ package org.scalasteward.core.forge.github -import io.circe.Decoder -import io.circe.generic.semiauto.deriveDecoder +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec case class RepositoriesOut(repositories: List[Repository]) object RepositoriesOut { - implicit val repositoriesDecoder: Decoder[RepositoriesOut] = deriveDecoder + implicit val repositoriesCodec: Codec[RepositoriesOut] = deriveCodec } case class Repository(full_name: String) object Repository { - implicit val repositoryDecoder: Decoder[Repository] = deriveDecoder + implicit val repositoryCodec: Codec[Repository] = deriveCodec } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/TokenOut.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/TokenOut.scala index 521aae8056..454c759ef8 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/TokenOut.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/github/TokenOut.scala @@ -16,10 +16,10 @@ package org.scalasteward.core.forge.github -import io.circe.Decoder -import io.circe.generic.semiauto.deriveDecoder +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec case class TokenOut(token: String) object TokenOut { - implicit val tokenDecoder: Decoder[TokenOut] = deriveDecoder + implicit val tokenCodec: Codec[TokenOut] = deriveCodec } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabAuthAlg.scala new file mode 100644 index 0000000000..993c0ffd31 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabAuthAlg.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2018-2023 Scala Steward contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.scalasteward.core.forge.gitlab + +import better.files.File +import cats.effect.Sync +import cats.syntax.all._ +import org.http4s.Uri.UserInfo +import org.http4s.{Header, Request, Uri} +import org.scalasteward.core.forge.BasicAuthAlg +import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg} +import org.typelevel.ci.CIStringSyntax + +class GitLabAuthAlg[F[_]](apiUri: Uri, login: String, gitAskPass: File)(implicit + F: Sync[F], + workspaceAlg: WorkspaceAlg[F], + processAlg: ProcessAlg[F] +) extends BasicAuthAlg[F](apiUri, login, gitAskPass) { + override def authenticateApi(req: Request[F]): F[Request[F]] = + userInfo.map { + case UserInfo(_, Some(password)) => req.putHeaders(Header.Raw(ci"Private-Token", password)) + case _ => req + } +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala index fd96bec299..fc167d5330 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/git/FileGitAlg.scala @@ -20,13 +20,14 @@ import better.files.File import cats.effect.MonadCancelThrow import cats.syntax.all._ import org.http4s.Uri -import org.scalasteward.core.application.Config.GitCfg +import org.scalasteward.core.application.Config +import org.scalasteward.core.forge.ForgeType._ import org.scalasteward.core.git.FileGitAlg.{dotdot, gitCmd} import org.scalasteward.core.io.process.{ProcessFailedException, SlurpOptions} import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} import org.scalasteward.core.util.Nel -final class FileGitAlg[F[_]](config: GitCfg)(implicit +final class FileGitAlg[F[_]](config: Config)(implicit fileAlg: FileAlg[F], processAlg: ProcessAlg[F], workspaceAlg: WorkspaceAlg[F], @@ -156,7 +157,15 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit repo: File, slurpOptions: SlurpOptions = Set.empty ): F[List[String]] = { - val extraEnv = List("GIT_ASKPASS" -> config.gitAskPass.pathAsString) + val extraEnv = (config.forgeCfg.tpe match { + case AzureRepos => Some(config.gitCfg.gitAskPass) + case Bitbucket => Some(config.gitCfg.gitAskPass) + case BitbucketServer => Some(config.gitCfg.gitAskPass) + case GitHub => None + case GitLab => Some(config.gitCfg.gitAskPass) + case Gitea => Some(config.gitCfg.gitAskPass) + }).map("GIT_ASKPASS" -> _.pathAsString).toList + processAlg .exec(gitCmd ++ args.toList, repo, extraEnv, slurpOptions) .recoverWith { @@ -175,10 +184,10 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit git(args: _*)(repo, SlurpOptions.ignoreBufferOverflow) private val sign: String = - if (config.signCommits) "--gpg-sign" else "--no-gpg-sign" + if (config.gitCfg.signCommits) "--gpg-sign" else "--no-gpg-sign" private def signoff(signoffCommits: Option[Boolean]): String = - if (signoffCommits.getOrElse(config.signoff)) "--signoff" else "--no-signoff" + if (signoffCommits.getOrElse(config.gitCfg.signoff)) "--signoff" else "--no-signoff" } object FileGitAlg { diff --git a/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala index f8f6507554..0b0e74ee3a 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/git/GenGitAlg.scala @@ -20,7 +20,7 @@ import cats.effect.{MonadCancel, MonadCancelThrow} import cats.syntax.all._ import cats.{FlatMap, Monad} import org.http4s.Uri -import org.scalasteward.core.application.Config.GitCfg +import org.scalasteward.core.application.Config import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} trait GenGitAlg[F[_], Repo] { @@ -178,7 +178,7 @@ trait GenGitAlg[F[_], Repo] { } object GenGitAlg { - def create[F[_]](config: GitCfg)(implicit + def create[F[_]](config: Config)(implicit fileAlg: FileAlg[F], processAlg: ProcessAlg[F], workspaceAlg: WorkspaceAlg[F], diff --git a/modules/core/src/test/scala/org/scalasteward/core/application/StewardAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/application/StewardAlgTest.scala index 1e686cac1d..9a17d4444d 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/application/StewardAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/application/StewardAlgTest.scala @@ -3,11 +3,15 @@ package org.scalasteward.core.application import cats.effect.ExitCode import munit.CatsEffectSuite import org.scalasteward.core.mock.MockContext.context.stewardAlg -import org.scalasteward.core.mock.{MockConfig, MockState} +import org.scalasteward.core.mock.{GitHubAuth, MockConfig, MockState} class StewardAlgTest extends CatsEffectSuite { test("runF") { - val exitCode = stewardAlg.runF.runA(MockState.empty.addUris(MockConfig.reposFile -> "")) + val exitCode = stewardAlg.runF.runA( + MockState.empty + .copy(clientResponses = GitHubAuth.api(List.empty)) + .addUris(MockConfig.reposFile -> "") + ) assertIO(exitCode, ExitCode.Success) } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoAlgTest.scala index 53f1a5e224..23ac537637 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoAlgTest.scala @@ -6,8 +6,9 @@ import org.scalasteward.core.data.Repo import org.scalasteward.core.forge.data.{RepoOut, UserOut} import org.scalasteward.core.git.Branch import org.scalasteward.core.mock.MockConfig.config -import org.scalasteward.core.mock.MockContext.context._ +import org.scalasteward.core.mock.MockContext.context.{gitAlg, logger, workspaceAlg} import org.scalasteward.core.mock.MockState.TraceEntry.{Cmd, Log} +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth import org.scalasteward.core.mock.{MockConfig, MockEff, MockState} class ForgeRepoAlgTest extends CatsEffectSuite { @@ -29,11 +30,12 @@ class ForgeRepoAlgTest extends CatsEffectSuite { Branch("main") ) - private val parentUrl = s"https://${config.forgeCfg.login}@github.com/fthomas/datapackage" - private val forkUrl = s"https://${config.forgeCfg.login}@github.com/scala-steward/datapackage" + private val parentUrl = "https://github.com/fthomas/datapackage" + private val forkUrl = "https://github.com/scala-steward/datapackage" test("cloneAndSync: doNotFork = false") { - val state = forgeRepoAlg.cloneAndSync(repo, forkRepoOut).runS(MockState.empty) + val obtained = + new ForgeRepoAlg[MockEff](config).cloneAndSync(repo, forkRepoOut).runS(MockState.empty) val expected = MockState.empty.copy( trace = Vector( Log("Clone scala-steward/datapackage"), @@ -58,7 +60,7 @@ class ForgeRepoAlgTest extends CatsEffectSuite { Cmd.git(repoDir, "submodule", "update", "--init", "--recursive") ) ) - state.map(assertEquals(_, expected)) + obtained.map(assertEquals(_, expected)) } test("cloneAndSync: doNotFork = true") { @@ -88,7 +90,7 @@ class ForgeRepoAlgTest extends CatsEffectSuite { } test("cloneAndSync: doNotFork = false, no parent") { - forgeRepoAlg + new ForgeRepoAlg[MockEff](config) .cloneAndSync(repo, parentRepoOut) .runS(MockState.empty) .attempt diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeSelectionTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeSelectionTest.scala deleted file mode 100644 index df4de5051b..0000000000 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeSelectionTest.scala +++ /dev/null @@ -1,35 +0,0 @@ -package org.scalasteward.core.forge - -import cats.Id -import munit.FunSuite -import org.http4s.headers.{Accept, Authorization} -import org.http4s.syntax.all._ -import org.http4s.{BasicCredentials, Headers, MediaType, Request} -import org.scalasteward.core.forge.ForgeType.GitHub -import org.scalasteward.core.forge.data.AuthenticatedUser -import org.scalasteward.core.mock.MockConfig - -class ForgeSelectionTest extends FunSuite { - test("authenticate") { - val obtained = ForgeSelection - .authenticate[Id](GitHub, AuthenticatedUser("user", "pass")) - .apply(Request(headers = Headers(Accept(MediaType.text.plain)))) - .headers - val expected = - Headers(Accept(MediaType.text.plain), Authorization(BasicCredentials("user", "pass"))) - assertEquals(obtained, expected) - } - - test("authenticateIfApiHost") { - val forgeCfg = MockConfig.config.forgeCfg - val auth = ForgeSelection.authenticateIfApiHost[Id](forgeCfg, AuthenticatedUser("user", "pass")) - - val obtained1 = auth.apply(Request(uri = uri"http://example.com/foo/bar")).headers - val expected1 = Headers(Authorization(BasicCredentials("user", "pass"))) - assertEquals(obtained1, expected1) - - val obtained2 = auth.apply(Request(uri = uri"http://acme.org/foo/bar")).headers - val expected2 = Headers() - assertEquals(obtained2, expected2) - } -} diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala index 5b486f164f..3b829d9e9f 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala @@ -1,11 +1,9 @@ package org.scalasteward.core.forge.azurerepos -import cats.syntax.semigroupk._ import munit.CatsEffectSuite import org.http4s.dsl.Http4sDsl -import org.http4s.headers.Authorization import org.http4s.implicits._ -import org.http4s.{BasicCredentials, HttpApp, Uri} +import org.http4s.{HttpApp, Uri} import org.scalasteward.core.TestInstances.ioLogger import org.scalasteward.core.application.Config.AzureReposCfg import org.scalasteward.core.data.Repo @@ -14,11 +12,10 @@ import org.scalasteward.core.forge.{ForgeSelection, ForgeType} import org.scalasteward.core.git.{Branch, Sha1} import org.scalasteward.core.mock.MockConfig.config import org.scalasteward.core.mock.MockContext.context.httpJsonClient +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth import org.scalasteward.core.mock.{MockEff, MockState} class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { - private val user = AuthenticatedUser("user", "pass") - private val userM = MockEff.pure(user) private val repo = Repo("scala-steward-org", "scala-steward") private val apiHost = uri"https://dev.azure.com" @@ -28,16 +25,9 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { object targetRefNameMatcher extends QueryParamDecoderMatcher[String]("searchCriteria.targetRefName") - private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) - private val auth = HttpApp[MockEff] { request => - (request: @unchecked) match { - case _ if !request.headers.get[Authorization].contains(basicAuth) => Forbidden() - } - } private val httpApp = HttpApp[MockEff] { case GET -> Root / "azure-org" / repo.owner / "_apis/git/repositories" / repo.repo => - Ok(""" - |{ + Ok("""{ | "id": "3846fbbd-71a0-402b-8352-6b1b9b2088aa", | "name": "scala-steward", | "url": "https://dev.azure.com/azure-org/scala-steward-org/_apis/git/repositories/scala-steward", @@ -53,8 +43,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case GET -> Root / "azure-org" / repo.owner / "_apis/git/repositories" / repo.repo / "stats/branches" :? branchNameMatcher("main") => - Ok(""" - |{ + Ok("""{ | "commit": { | "commitId": "f55c9900528e548511fbba6874c873d44c5d714c" | }, @@ -66,8 +55,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case POST -> Root / "azure-org" / repo.owner / "_apis/git/repositories" / repo.repo / "pullrequests" => Created( - """ - |{ + """{ | "repository": { | "id": "3846fbbd-71a0-402b-8352-6b1b9b2088aa", | "name": "scala-steward", @@ -93,8 +81,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case GET -> Root / "azure-org" / repo.owner / "_apis/git/repositories" / repo.repo / "pullrequests" :? sourceRefNameMatcher("refs/heads/update/cats-effect-3.3.14") +& targetRefNameMatcher("refs/heads/main") => - Ok(""" - |{ + Ok("""{ | "value":[ | { | "pullRequestId":26, @@ -118,8 +105,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { |}""".stripMargin) case PATCH -> Root / "azure-org" / repo.owner / "_apis/git/repositories" / repo.repo / "pullrequests" / "26" => - Ok(""" - |{ + Ok("""{ | "repository": { | "id": "3846fbbd-71a0-402b-8352-6b1b9b2088aa", | "name": "scala-steward", @@ -142,8 +128,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { |}""".stripMargin) case POST -> Root / "azure-org" / repo.owner / "_apis/git/repositories" / repo.repo / "pullrequests" / "26" / "threads" => - Ok(""" - |{ + Ok("""{ | "id":17, | "publishedDate":"2022-07-24T22:06:00.067Z", | "lastUpdatedDate":"2022-07-24T22:06:00.067Z", @@ -161,8 +146,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { |}""".stripMargin) case POST -> Root / "azure-org" / repo.owner / "_apis/git/repositories" / repo.repo / "pullrequests" / "26" / "labels" => - Ok(""" - |{ + Ok("""{ | "id": "921dbff4-9c00-49d6-9262-ab0d6e4a13f1", | "name": "dependency-updates", | "active": true, @@ -172,11 +156,12 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) + private val state = MockState.empty.copy(clientResponses = httpApp) private val forgeCfg = config.forgeCfg.copy(apiHost = apiHost, tpe = ForgeType.AzureRepos) private val azureReposCfg = AzureReposCfg(organization = Some("azure-org")) - private val azureReposApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, azureReposCfg, userM) + private val azureReposApiAlg = + ForgeSelection.forgeApiAlg[MockEff](forgeCfg, azureReposCfg, noAuth.authenticateApi) test("getRepo") { val obtained = azureReposApiAlg.getRepo(repo).runA(state) diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala index 4c06bc6824..ee9102423b 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala @@ -1,12 +1,10 @@ package org.scalasteward.core.forge.bitbucket -import cats.syntax.semigroupk._ import io.circe.literal._ import munit.CatsEffectSuite import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -import org.http4s.headers.Authorization import org.http4s.implicits._ import org.scalasteward.core.TestInstances.ioLogger import org.scalasteward.core.application.Config.BitbucketCfg @@ -17,18 +15,9 @@ import org.scalasteward.core.git._ import org.scalasteward.core.mock.MockConfig.config import org.scalasteward.core.mock.MockContext.context.httpJsonClient import org.scalasteward.core.mock.{MockEff, MockState} +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth class BitbucketApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { - - private val user = AuthenticatedUser("user", "pass") - private val userM = MockEff.pure(user) - - private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) - private val auth = HttpApp[MockEff] { request => - (request: @unchecked) match { - case _ if !request.headers.get[Authorization].contains(basicAuth) => Forbidden() - } - } private val httpApp = HttpApp[MockEff] { case GET -> Root / "repositories" / "fthomas" / "base.g8" => Ok( @@ -206,11 +195,13 @@ class BitbucketApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { }""") case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) + private val state = MockState.empty.copy(clientResponses = httpApp) - private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.Bitbucket) + private val forgeCfg = + config.forgeCfg.copy(tpe = ForgeType.Bitbucket, apiHost = uri"https://bitbucket.org") private val bitbucketCfg = BitbucketCfg(useDefaultReviewers = true) - private val bitbucketApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, bitbucketCfg, userM) + private val bitbucketApiAlg = + ForgeSelection.forgeApiAlg[MockEff](forgeCfg, bitbucketCfg, noAuth.authenticateApi) private val prUrl = uri"https://bitbucket.org/fthomas/base.g8/pullrequests/2" private val repo = Repo("fthomas", "base.g8") diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala index a8a6bf691e..75dfb4fc52 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala @@ -1,11 +1,9 @@ package org.scalasteward.core.forge.bitbucketserver -import cats.syntax.semigroupk._ import munit.CatsEffectSuite import org.http4s.dsl.Http4sDsl -import org.http4s.headers.Authorization import org.http4s.implicits._ -import org.http4s.{BasicCredentials, HttpApp, Uri} +import org.http4s.{HttpApp, Uri} import org.scalasteward.core.TestInstances.ioLogger import org.scalasteward.core.application.Config.BitbucketServerCfg import org.scalasteward.core.data.Repo @@ -15,21 +13,14 @@ import org.scalasteward.core.git.{Branch, Sha1} import org.scalasteward.core.mock.MockConfig.config import org.scalasteward.core.mock.MockContext.context.httpJsonClient import org.scalasteward.core.mock.{MockEff, MockState} +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { object FilterTextMatcher extends QueryParamDecoderMatcher[String]("filterText") private val repo = Repo("scala-steward-org", "scala-steward") private val main = Branch("main") - private val user = AuthenticatedUser("user", "pass") - private val userM = MockEff.pure(user) - - private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) - private val auth = HttpApp[MockEff] { request => - (request: @unchecked) match { - case _ if !request.headers.get[Authorization].contains(basicAuth) => Forbidden() - } - } + private val httpApp = HttpApp[MockEff] { case GET -> Root / "rest" / "default-reviewers" / "1.0" / "projects" / repo.owner / "repos" / repo.repo / "conditions" => Ok(s"""[ @@ -109,11 +100,15 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) + private val state = MockState.empty.copy(clientResponses = httpApp) private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.BitbucketServer) private val bitbucketServerApiAlg = ForgeSelection - .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = false), userM) + .forgeApiAlg[MockEff]( + forgeCfg, + BitbucketServerCfg(useDefaultReviewers = false), + noAuth.authenticateApi + ) test("createPullRequest") { val data = NewPullRequestData( @@ -147,7 +142,11 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] reviewers = Nil ) val apiAlg = ForgeSelection - .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = true), userM) + .forgeApiAlg[MockEff]( + forgeCfg, + BitbucketServerCfg(useDefaultReviewers = true), + noAuth.authenticateApi + ) val pr = apiAlg.createPullRequest(repo, data).runA(state) val expected = PullRequestOut( diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala index dcfa749814..e3c764e0e3 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala @@ -1,13 +1,11 @@ package org.scalasteward.core.forge.gitea -import cats.syntax.semigroupk._ import io.circe.literal._ import munit.CatsEffectSuite import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -import org.http4s.headers.Authorization import org.http4s.implicits._ -import org.http4s.{BasicCredentials, HttpApp} +import org.http4s.HttpApp import org.scalasteward.core.TestInstances.ioLogger import org.scalasteward.core.application.Config.GiteaCfg import org.scalasteward.core.data.Repo @@ -17,19 +15,11 @@ import org.scalasteward.core.git.{Branch, Sha1} import org.scalasteward.core.mock.MockConfig.config import org.scalasteward.core.mock.MockContext.context.httpJsonClient import org.scalasteward.core.mock.{MockEff, MockState} +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth class GiteaApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { - private val user = AuthenticatedUser("user", "pass") - private val userM = MockEff.pure(user) private val repo = Repo("foo", "baz") - private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) - private val auth = HttpApp[MockEff] { request => - (request: @unchecked) match { - case _ if !request.headers.get[Authorization].contains(basicAuth) => Forbidden() - } - } - object PageQ extends QueryParamDecoderMatcher[Int]("page") private val httpApp = HttpApp[MockEff] { @@ -56,13 +46,14 @@ class GiteaApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) + private val state = MockState.empty.copy(clientResponses = httpApp) private val forgeCfg = config.forgeCfg.copy( tpe = ForgeType.Gitea, apiHost = config.forgeCfg.apiHost / "api" / "v1" ) - private val giteaAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GiteaCfg(), userM) + private val giteaAlg = + ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GiteaCfg(), noAuth.authenticateApi) test("getRepo") { giteaAlg diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala index 0c48e4a334..8ca1e034a2 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala @@ -7,9 +7,8 @@ import io.circe.Json import munit.CatsEffectSuite import org.http4s.circe._ import org.http4s.dsl.Http4sDsl -import org.http4s.headers.Authorization import org.http4s.implicits._ -import org.http4s.{BasicCredentials, HttpApp} +import org.http4s.HttpApp import org.scalasteward.core.TestInstances.ioLogger import org.scalasteward.core.application.Config.GitHubCfg import org.scalasteward.core.data.Repo @@ -18,19 +17,10 @@ import org.scalasteward.core.forge.{ForgeSelection, ForgeType} import org.scalasteward.core.git.{Branch, Sha1} import org.scalasteward.core.mock.MockConfig.config import org.scalasteward.core.mock.MockContext.context.httpJsonClient +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth import org.scalasteward.core.mock.{MockEff, MockState} class GitHubApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { - - private val user = AuthenticatedUser("user", "pass") - private val userM = MockEff.pure(user) - - private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) - private val auth = HttpApp[MockEff] { request => - (request: @unchecked) match { - case _ if !request.headers.get[Authorization].contains(basicAuth) => Forbidden() - } - } private val httpApp = HttpApp[MockEff] { case GET -> Root / "repos" / "fthomas" / "base.g8" => Ok( @@ -196,10 +186,11 @@ class GitHubApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) + private val state = MockState.empty.copy(clientResponses = httpApp) private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.GitHub) - private val gitHubApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GitHubCfg(), userM) + private val gitHubApiAlg = + ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GitHubCfg(), noAuth.authenticateApi) private val repo = Repo("fthomas", "base.g8") diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAppApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAppApiAlgTest.scala deleted file mode 100644 index 50574dec64..0000000000 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAppApiAlgTest.scala +++ /dev/null @@ -1,77 +0,0 @@ -package org.scalasteward.core.forge.github - -import io.circe.literal._ -import munit.CatsEffectSuite -import org.http4s._ -import org.http4s.circe._ -import org.http4s.dsl.Http4sDsl -import org.http4s.headers.Authorization -import org.scalasteward.core.mock.MockConfig.config -import org.scalasteward.core.mock.MockContext.context.httpJsonClient -import org.scalasteward.core.mock.{MockEff, MockState} -import org.typelevel.ci.CIStringSyntax - -class GitHubAppApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { - - object PerPageMatcher extends QueryParamDecoderMatcher[Int]("per_page") - - private def hasAuthHeader(req: Request[MockEff], authorization: Authorization): Boolean = - req.headers.get[Authorization].contains(authorization) - - private val jwtToken = "jwt-token-abc123" - private val ghsToken = "ghs_16C7e42F292c6912E7710c838347Ae178B4a" - private val jwtAuth = Authorization(Credentials.Token(AuthScheme.Bearer, jwtToken)) - private val tokenAuth = Authorization(Credentials.Token(ci"token", ghsToken)) - - private val state = MockState.empty.copy(clientResponses = HttpApp { - case req @ GET -> Root / "app" / "installations" :? PerPageMatcher(100) - if hasAuthHeader(req, jwtAuth) => - Ok(json"""[ - { - "id": 1 - }, - { - "id": 2 - } - ]""") - case req @ POST -> Root / "app" / "installations" / "1" / "access_tokens" - if hasAuthHeader(req, jwtAuth) => - Ok(json"""{ - "token": ${ghsToken} - }""") - - case req @ GET -> Root / "installation" / "repositories" :? PerPageMatcher(100) - if hasAuthHeader(req, tokenAuth) => - Ok(json"""{ - "repositories": [ - { - "full_name": "fthomas/base.g8" - }, - { - "full_name": "octocat/Hello-World" - } - ] - }""") - case _ => NotFound() - }) - - private val gitHubAppApiAlg = new GitHubAppApiAlg[MockEff](config.forgeCfg.apiHost) - - test("installations") { - val installations = gitHubAppApiAlg.installations(jwtToken).runA(state) - assertIO(installations, List(InstallationOut(1), InstallationOut(2))) - } - - test("accessToken") { - val token = gitHubAppApiAlg.accessToken(jwtToken, 1).runA(state) - assertIO(token, TokenOut(ghsToken)) - } - - test("repositories") { - val repositories = gitHubAppApiAlg.repositories(ghsToken).runA(state) - assertIO( - repositories, - RepositoriesOut(List(Repository("fthomas/base.g8"), Repository("octocat/Hello-World"))) - ) - } -} diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAuthAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAuthAlgTest.scala index a12c3dddaf..f2e13551c4 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAuthAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAuthAlgTest.scala @@ -1,28 +1,95 @@ package org.scalasteward.core.forge.github import better.files.File -import cats.effect.IO +import io.circe.literal.JsonStringContext import munit.CatsEffectSuite - +import org.http4s.{AuthScheme, Credentials, HttpApp, Request} +import org.http4s.dsl.Http4sDsl +import org.http4s.circe._ +import org.http4s.headers.Authorization +import org.http4s.implicits.http4sLiteralsSyntax +import org.scalasteward.core.data.Repo +import org.scalasteward.core.mock.{MockEff, MockState} +import org.scalasteward.core.mock.MockContext.context.httpJsonClient +import org.scalasteward.core.mock.MockContext.context.logger +import org.typelevel.ci.CIStringSyntax import scala.concurrent.duration._ -class GitHubAuthAlgTest extends CatsEffectSuite { +class GitHubAuthAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { + object PerPageMatcher extends QueryParamDecoderMatcher[Int]("per_page") + + private def hasAuthHeader(req: Request[MockEff], authorization: Authorization): Boolean = + req.headers.get[Authorization].contains(authorization) - private val gitHubAuthAlg = GitHubAuthAlg.create[IO] private val pemFile = File(getClass.getResource("/rsa-4096-private.pem")) + private val gitHubAuthAlg = new GitHubAuthAlg[MockEff](uri"http://localhost", 42L, pemFile) private val nowMillis = 1673743729714L + private val jwtToken = "jwt-token-abc123" + private val ghsToken = "ghs_16C7e42F292c6912E7710c838347Ae178B4a" + private val jwtAuth = Authorization(Credentials.Token(AuthScheme.Bearer, jwtToken)) + private val tokenAuth = Authorization(Credentials.Token(ci"token", ghsToken)) + + private val state = MockState.empty.copy(clientResponses = HttpApp { + case req @ GET -> Root / "app" / "installations" :? PerPageMatcher(100) + if hasAuthHeader(req, jwtAuth) => + Ok(json"""[ + { + "id": 1 + }, + { + "id": 2 + } + ]""") + case req @ POST -> Root / "app" / "installations" / "1" / "access_tokens" + if hasAuthHeader(req, jwtAuth) => + Ok(json"""{ + "token": $ghsToken + }""") + + case req @ GET -> Root / "installation" / "repositories" :? PerPageMatcher(100) + if hasAuthHeader(req, tokenAuth) => + Ok(json"""{ + "repositories": [ + { + "full_name": "fthomas/base.g8" + }, + { + "full_name": "octocat/Hello-World" + } + ] + }""") + case _ => NotFound() + }) test("createJWT with ttl") { - val obtained = gitHubAuthAlg.createJWT(GitHubApp(42, pemFile), 2.minutes, nowMillis) + val obtained = gitHubAuthAlg.createJWT(2.minutes, nowMillis).runA(state) val expected = "eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2NzM3NDM3MjksImlzcyI6IjQyIiwiZXhwIjoxNjczNzQzODQ5fQ.SDW4TqjokzYAwHD6joDdgqCtQyPrq-4QThanWB12vNUkjNtP4gw9iiG_baWBNXi4nlA6_HtO0H_WNKO6God6vkHz_ERBbIUb7I2vhp17NEb8vRECUksqARnrAzPU8HPUZPD5V7uehEDxEa-Tv-eI3L8iH8JVWx-m60vAZdBi76IQ094mIXf_d1TC75HKpap1wPMV7i_973IVAuL6zu2Sy6bkhHAS0WAQKStSAolFvwih7uq2f6N1b-1ogopFtkL6w19lQ4iRSvaoXPvkyBuvw6DqowVcAWon8-OB9cdzUIsjQs5GkR4IwCQQOBp-9_NYKBRDyVTwa-vqBBlYcOc_Zzd-_tpK3zRLpsh-h8_p0W8YAQrYAVyJRWn128Mm72jc2q9DkWhsiIGGWr44p3z6DENypgx3HiFDZbcvgMhPJKeNY3CwYh2QK56XtPNcbYSmUzog1IkX5lrM3WOO9j1bfj8tTP5h46dYXApvTq2-q5zlLP66Rm40RQnc_TE_6ntVq1kKn6IQ0yqEuPN0GVwoX71PElnajufz_Bzn08-YtYMK2Ca-t-wKWapDaH9zDjWUoXe_Pbcb5T_AZkbqPy8MHkzRzkMFSACwrXjHDuq_PphdlHZJeIb4xJ0PSp4f6urz_TRdxFmrTlG-e7DaKcoOLMbp8VK419TD3VinXq3MGDs" assertIO(obtained, expected) } test("createJWT without ttl") { - val obtained = gitHubAuthAlg.createJWT(GitHubApp(42, pemFile), 0.minutes, nowMillis) + val obtained = gitHubAuthAlg.createJWT(0.minutes, nowMillis).runA(state) val expected = "eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2NzM3NDM3MjksImlzcyI6IjQyIn0.GcJ2RzzwgN-decPz0BNhNwrMFh6Wjj2xtbH0bOWEBGolnclEymJDT0QrjojvVw7iDabq5FezOGgPYP6JXlykMQlXFjX7TFeBAsydpZt1wyU1N8PQwxpoUtumksBGgTqNuIWg6_Y8CQg-UTbM4B63axcNREz6iT43a0cKxNe0ABy6jwcWSXw2Ck5Ob2uS_ZMCAt3VapIovT7Vci0goI7z6eXF8l6FpJauSgiVRXYsOAoZwXnDeNU1LkWFkGtWh9vK4iyaI_IDc85f3ODU5KfiPHOWuy2h7j6WPKEMXQTLXiiGQr_HqP4ROR-HXW7hlpyBFsrL44EqNe3oQcnTWNdOAj2s2K0aLzMm1XmeenPKgMeJcDvp8q_lRFKC54En4bHKZZEccOVnfItEb7D7fkBuWUYM5-k6cb4CPZyPrOvO5zBsQyboW2_Zcrpr_mGelm9rdSQ29azIvu2G2gBWY_QsT54E1_D3uN4HbsUsTxwjJPXlw2ScFgn_4wGu3XuU9QfIzipw4-PJtXo9deoHMinji0VuXzAZslJMyCoKqvCOV7voVNQOuQJroVeahVY1cU-dWLWOfrOcJ0LZRxZ2gIoRztc1wawfmNix8mFGNXei_qY0M5LZtOgWfdgIsmrUF17s1mX2Lwp2mlvjvCCP6qcXQnrn6GWit_ihcOb2IFR9yIw" assertIO(obtained, expected) } + + test("installations") { + val installations = gitHubAuthAlg.installations(jwtToken).runA(state) + assertIO(installations, List(InstallationOut(1), InstallationOut(2))) + } + + test("accessToken") { + val token = gitHubAuthAlg.accessToken(jwtToken, 1).runA(state) + assertIO(token, TokenOut(ghsToken)) + } + + test("repositories") { + val repositories = gitHubAuthAlg.repositories(ghsToken).runA(state) + assertIO( + repositories, + List(Repo("fthomas", "base.g8"), Repo("octocat", "Hello-World")) + ) + } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala index 20c9fa128b..80cf5fed35 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala @@ -20,15 +20,11 @@ import org.scalasteward.core.forge.{ForgeSelection, ForgeType} import org.scalasteward.core.git.{Branch, Sha1} import org.scalasteward.core.mock.MockConfig.config import org.scalasteward.core.mock.MockContext.context.httpJsonClient +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth import org.scalasteward.core.mock.{MockEff, MockState} import org.scalasteward.core.repoconfig.RepoConfig -import org.typelevel.ci.CIStringSyntax class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { - - private val user = AuthenticatedUser("user", "pass") - private val userM = MockEff.pure(user) - object MergeWhenPipelineSucceedsMatcher extends QueryParamDecoderMatcher[Boolean]("merge_when_pipeline_succeeds") @@ -36,15 +32,6 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { object UsernameMatcher extends QueryParamDecoderMatcher[String]("username") - private val auth = HttpApp[MockEff] { request => - (request: @unchecked) match { - case _ - if !request.headers - .get(ci"Private-Token") - .exists(nel => nel.head.value == user.accessToken) => - Forbidden() - } - } private val httpApp = HttpApp[MockEff] { case POST -> Root / "projects" / "foo/bar" / "fork" => @@ -119,7 +106,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) + private val state = MockState.empty.copy(clientResponses = httpApp) private val gitlabApiAlg = ForgeSelection.forgeApiAlg[MockEff]( config.forgeCfg.copy(tpe = ForgeType.GitLab), @@ -128,7 +115,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = false ), - userM + noAuth.authenticateApi ) private val gitlabApiAlgNoFork = ForgeSelection.forgeApiAlg[MockEff]( @@ -138,7 +125,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = false ), - userM + noAuth.authenticateApi ) private val gitlabApiAlgAutoMerge = ForgeSelection.forgeApiAlg[MockEff]( @@ -148,7 +135,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = false ), - userM + noAuth.authenticateApi ) private val gitlabApiAlgRemoveSourceBranch = ForgeSelection.forgeApiAlg[MockEff]( @@ -158,7 +145,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = true ), - userM + noAuth.authenticateApi ) private val gitlabApiAlgLessReviewersRequired = ForgeSelection.forgeApiAlg[MockEff]( @@ -168,7 +155,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = Some(0), removeSourceBranch = false ), - userM + noAuth.authenticateApi ) private val gitlabApiAlgWithAssigneeAndReviewers = ForgeSelection.forgeApiAlg[MockEff]( @@ -178,7 +165,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = Some(0), removeSourceBranch = false ), - userM + noAuth.authenticateApi ) private val data = UpdateData( @@ -291,7 +278,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { MethodNotAllowed(Allow(OPTIONS, GET, HEAD)) } } - val localState = MockState.empty.copy(clientResponses = auth <+> localApp <+> httpApp) + val localState = MockState.empty.copy(clientResponses = localApp <+> httpApp) val prOut = gitlabApiAlgNoFork .createPullRequest(Repo("foo", "bar"), newPRData) @@ -330,7 +317,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { BadRequest(s"Cannot set requiredReviewers to $requiredReviewers") } } - val localState = MockState.empty.copy(clientResponses = auth <+> localApp <+> httpApp) + val localState = MockState.empty.copy(clientResponses = localApp <+> httpApp) val prOut = gitlabApiAlgLessReviewersRequired .createPullRequest(Repo("foo", "bar"), newPRData) diff --git a/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala index 2d3561f2fe..98eae0a32d 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/git/FileGitAlgTest.scala @@ -286,7 +286,7 @@ object FileGitAlgTest { WorkspaceAlg.create[IO](config) implicit val ioGitAlg: GenGitAlg[IO, File] = - new FileGitAlg[IO](config.gitCfg).contramapRepoF(IO.pure) + new FileGitAlg[IO](config).contramapRepoF(IO.pure) val ioAuxGitAlg: AuxGitAlg[IO] = new AuxGitAlg[IO] diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/GitHubAuth.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/GitHubAuth.scala new file mode 100644 index 0000000000..7bab97f20e --- /dev/null +++ b/modules/core/src/test/scala/org/scalasteward/core/mock/GitHubAuth.scala @@ -0,0 +1,19 @@ +package org.scalasteward.core.mock + +import io.circe.syntax.EncoderOps +import org.http4s.dsl.Http4sDsl +import org.http4s.HttpApp +import org.scalasteward.core.forge.github.{InstallationOut, RepositoriesOut, Repository, TokenOut} + +object GitHubAuth extends Http4sDsl[MockEff] { + def api(repositories: List[Repository]): HttpApp[MockEff] = HttpApp[MockEff] { req => + (req: @unchecked) match { + case GET -> Root / "app" / "installations" => + Ok(List(InstallationOut(1L)).asJson.spaces2) + case POST -> Root / "app" / "installations" / "1" / "access_tokens" => + Ok(TokenOut("some-token").asJson.spaces2) + case GET -> Root / "installation" / "repositories" => + Ok(RepositoriesOut(repositories).asJson.spaces2) + } + } +} diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala index 955eb6641c..c0726eca6b 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala @@ -4,11 +4,17 @@ import better.files.File import org.http4s.Uri import org.scalasteward.core.application.Cli import org.scalasteward.core.application.Cli.ParseResult.Success +import scala.io.Source object MockConfig { val mockRoot: File = File.temp / "scala-steward" val reposFile: Uri = Uri.unsafeFromString((mockRoot / "repos.md").pathAsString) - mockRoot.delete(true) // Ensure folder is cleared of previous test files + mockRoot.delete(swallowIOExceptions = true) // Ensure folder is cleared of previous test files + + mockRoot.createDirectory() + val key = mockRoot / "rsa-4096-private.pem" + key.overwrite(Source.fromResource("rsa-4096-private.pem").mkString) + private val args: List[String] = List( s"--workspace=$mockRoot/workspace", s"--repos-file=$reposFile", @@ -22,6 +28,8 @@ object MockConfig { "--env-var=VAR2=val2", "--cache-ttl=1hour", "--add-labels", + "--github-app-id=1234", + s"--github-app-key-file=$key", "--refresh-backoff-period=1hour" ) val Success(Cli.Usage.Regular(config)) = Cli.parseArgs(args) diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockForgeAuthAlg.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockForgeAuthAlg.scala new file mode 100644 index 0000000000..499f7978cd --- /dev/null +++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockForgeAuthAlg.scala @@ -0,0 +1,14 @@ +package org.scalasteward.core.mock + +import org.http4s.{Request, Uri} +import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.ForgeAuthAlg + +object MockForgeAuthAlg { + implicit val noAuth: ForgeAuthAlg[MockEff] = new ForgeAuthAlg[MockEff] { + override def authenticateApi(req: Request[MockEff]): MockEff[Request[MockEff]] = + MockEff.pure(req) + override def authenticateGit(uri: Uri): MockEff[Uri] = MockEff.pure(uri) + override def accessibleRepos: MockEff[List[Repo]] = MockEff.pure(List.empty) + } +} diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockState.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockState.scala index a3ad28f5ee..417203a36a 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/mock/MockState.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockState.scala @@ -7,7 +7,6 @@ import org.http4s.{HttpApp, Uri} import org.scalasteward.core.git.FileGitAlg import org.scalasteward.core.git.FileGitAlgTest.ioAuxGitAlg import org.scalasteward.core.io.FileAlgTest.ioFileAlg -import org.scalasteward.core.mock.MockConfig.mockRoot import org.scalasteward.core.mock.MockState.TraceEntry import org.scalasteward.core.mock.MockState.TraceEntry.{Cmd, Log} @@ -85,7 +84,7 @@ object MockState { def git(repoDir: File, args: String*): Cmd = { val env = - List(s"GIT_ASKPASS=$mockRoot/askpass.sh", "VAR1=val1", "VAR2=val2", repoDir.toString) + List("VAR1=val1", "VAR2=val2", repoDir.toString) Cmd(env ++ FileGitAlg.gitCmd.toList ++ args) } diff --git a/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala index f03182c741..1cdf3e6e6c 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/nurture/NurtureAlgTest.scala @@ -1,5 +1,6 @@ package org.scalasteward.core.nurture +import cats.syntax.all._ import munit.CatsEffectSuite import org.http4s.HttpApp import org.http4s.dsl.Http4sDsl @@ -10,7 +11,7 @@ import org.scalasteward.core.edit.EditAttempt.UpdateEdit import org.scalasteward.core.forge.data.NewPullRequestData import org.scalasteward.core.git.{Branch, Commit} import org.scalasteward.core.mock.MockContext.context -import org.scalasteward.core.mock.{MockConfig, MockEff, MockState} +import org.scalasteward.core.mock.{GitHubAuth, MockConfig, MockEff, MockState} import org.scalasteward.core.repoconfig.{PullRequestsConfig, RepoConfig} class NurtureAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { @@ -29,7 +30,7 @@ class NurtureAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { val updateBranch = Branch("update/cats-effect-3.4.0") val updateData = UpdateData(repoData, fork, update, baseBranch, dummySha1, updateBranch) val edits = List(UpdateEdit(update, Commit(dummySha1))) - val state = MockState.empty.copy(clientResponses = HttpApp { + val state = MockState.empty.copy(clientResponses = GitHubAuth.api(List.empty) <+> HttpApp { case HEAD -> Root / "typelevel" / "cats-effect" => Ok() case HEAD -> Root / "typelevel" / "cats-effect" / "releases" / "tag" / "v3.4.0" => Ok() case HEAD -> Root / "typelevel" / "cats-effect" / "compare" / "v3.3.0...v3.4.0" => Ok() diff --git a/modules/core/src/test/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinderTest.scala b/modules/core/src/test/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinderTest.scala index e51ef26f44..5d5c0de262 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinderTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinderTest.scala @@ -9,21 +9,24 @@ import org.scalasteward.core.application.Config.ForgeCfg import org.scalasteward.core.coursier.DependencyMetadata import org.scalasteward.core.data.Version import org.scalasteward.core.forge.ForgeType._ +import org.scalasteward.core.forge.github.Repository import org.scalasteward.core.forge.{ForgeRepo, ForgeType} import org.scalasteward.core.mock.MockContext.context._ -import org.scalasteward.core.mock.{MockEff, MockState} +import org.scalasteward.core.mock.{GitHubAuth, MockEff, MockState} import org.scalasteward.core.nurture.UpdateInfoUrl._ import org.scalasteward.core.nurture.UpdateInfoUrlFinder._ class UpdateInfoUrlFinderTest extends CatsEffectSuite with Http4sDsl[MockEff] { - private val state = MockState.empty.copy(clientResponses = HttpApp { + private val httpApp = HttpApp[MockEff] { case HEAD -> Root / "foo" / "bar" / "README.md" => Ok() case HEAD -> Root / "foo" / "bar" / "compare" / "v0.1.0...v0.2.0" => Ok() case HEAD -> Root / "foo" / "bar1" / "blob" / "master" / "RELEASES.md" => Ok() case HEAD -> Root / "foo" / "buz" / "compare" / "v0.1.0...v0.2.0" => PermanentRedirect() case HEAD -> Root / "foo" / "bar2" / "releases" / "tag" / "v0.2.0" => Ok() case _ => NotFound() - }) + } + private val authApp = GitHubAuth.api(List(Repository("foo/bar"))) + private val state = MockState.empty.copy(clientResponses = authApp <+> httpApp) private val v1 = Version("0.1.0") private val v2 = Version("0.2.0") diff --git a/modules/core/src/test/scala/org/scalasteward/core/repocache/RepoCacheAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/repocache/RepoCacheAlgTest.scala index d22ee7dd02..069300cde8 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/repocache/RepoCacheAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/repocache/RepoCacheAlgTest.scala @@ -1,7 +1,6 @@ package org.scalasteward.core.repocache -import io.circe.Encoder -import io.circe.generic.semiauto +import cats.implicits.toSemigroupKOps import io.circe.syntax._ import munit.CatsEffectSuite import org.http4s.HttpApp @@ -11,20 +10,15 @@ import org.http4s.syntax.all._ import org.scalasteward.core.TestInstances.dummySha1 import org.scalasteward.core.data.{Repo, RepoData} import org.scalasteward.core.forge.data.{RepoOut, UserOut} +import org.scalasteward.core.forge.github.Repository import org.scalasteward.core.git.Branch -import org.scalasteward.core.mock.MockContext.context._ -import org.scalasteward.core.mock.{MockEff, MockState} +import org.scalasteward.core.mock.MockContext.context.{repoCacheAlg, repoConfigAlg, workspaceAlg} +import org.scalasteward.core.mock.{GitHubAuth, MockEff, MockState} import org.scalasteward.core.util.intellijThisImportIsUsed class RepoCacheAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { intellijThisImportIsUsed(encodeUri) - implicit val userOutEncoder: Encoder[UserOut] = - semiauto.deriveEncoder - - implicit val repoOutEncoder: Encoder[RepoOut] = - semiauto.deriveEncoder - test("checkCache: up-to-date cache") { val repo = Repo("typelevel", "cats-effect") val repoOut = RepoOut( @@ -44,14 +38,16 @@ class RepoCacheAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { ) val repoCache = RepoCache(dummySha1, Nil, None, None) val workspace = workspaceAlg.rootDir.unsafeRunSync() + val httpApp = HttpApp[MockEff] { + case POST -> Root / "repos" / "typelevel" / "cats-effect" / "forks" => + Ok(repoOut.asJson.spaces2) + case GET -> Root / "repos" / "typelevel" / "cats-effect" / "branches" / "main" => + Ok(s""" { "name": "main", "commit": { "sha": "${dummySha1.value}" } } """) + case _ => NotFound() + } + val authApp = GitHubAuth.api(List(Repository("typelevel/cats-effect"))) val state = MockState.empty - .copy(clientResponses = HttpApp[MockEff] { - case POST -> Root / "repos" / "typelevel" / "cats-effect" / "forks" => - Ok(repoOut.asJson.spaces2) - case GET -> Root / "repos" / "typelevel" / "cats-effect" / "branches" / "main" => - Ok(s""" { "name": "main", "commit": { "sha": "${dummySha1.value}" } } """) - case _ => NotFound() - }) + .copy(clientResponses = authApp <+> httpApp) .addFiles( workspace / "store/repo_cache/v1/github/typelevel/cats-effect/repo_cache.json" -> repoCache.asJson.spaces2 )