From b0c1bf90c983824c22be4c0e055fe55cdd3ec4d3 Mon Sep 17 00:00:00 2001 From: Joan Goyeau Date: Mon, 30 Sep 2024 22:40:33 -0400 Subject: [PATCH] Refactor Auth to make GitHub Enterprise work --- .../scalasteward/core/application/Cli.scala | 184 ++++++++----- .../core/application/Config.scala | 57 +--- .../core/application/Context.scala | 29 +- .../core/application/ExitCodePolicy.scala | 2 +- .../core/application/StewardAlg.scala | 27 +- .../core/coursier/DependencyMetadata.scala | 7 +- .../org/scalasteward/core/data/Version.scala | 2 +- .../core/forge/BasicAuthAlg.scala | 55 ++++ .../org/scalasteward/core/forge/Forge.scala | 129 +++++++++ .../scalasteward/core/forge/ForgeApiAlg.scala | 38 ++- .../core/forge/ForgeAuthAlg.scala | 62 +++-- .../scalasteward/core/forge/ForgeRepo.scala | 75 +++++- .../core/forge/ForgeRepoAlg.scala | 39 +-- .../core/forge/ForgeSelection.scala | 98 ------- .../scalasteward/core/forge/ForgeType.scala | 149 ---------- .../forge/azurerepos/AzureReposApiAlg.scala | 9 +- .../forge/bitbucket/BitbucketApiAlg.scala | 13 +- .../BitbucketServerApiAlg.scala | 11 +- .../BitbucketServerAuthAlg.scala | 45 ++++ .../core/forge/data/RepoOut.scala | 6 +- .../core/forge/data/UserOut.scala | 5 +- .../core/forge/gitea/GiteaApiAlg.scala | 6 +- .../core/forge/github/GitHubApiAlg.scala | 40 +-- .../core/forge/github/GitHubApp.scala | 21 -- .../core/forge/github/GitHubAppApiAlg.scala | 84 ------ .../core/forge/github/GitHubAuthAlg.scala | 182 +++++++++---- .../core/forge/github/InstallationOut.scala | 5 +- .../core/forge/github/RepositoriesOut.scala | 8 +- .../core/forge/github/TokenOut.scala | 6 +- .../core/forge/gitlab/GitLabApiAlg.scala | 19 +- .../core/forge/gitlab/GitLabAuthAlg.scala | 38 +++ .../scalasteward/core/git/FileGitAlg.scala | 37 ++- .../org/scalasteward/core/git/GenGitAlg.scala | 4 +- .../core/nurture/NurtureAlg.scala | 13 +- .../core/nurture/UpdateInfoUrlFinder.scala | 26 +- .../core/repocache/RepoCacheAlg.scala | 2 +- .../scalasteward/core/util/UrlChecker.scala | 47 ++-- .../core/application/CliTest.scala | 254 ++++++++++-------- .../core/application/RunResultsTest.scala | 1 + .../core/application/StewardAlgTest.scala | 8 +- .../core/buildtool/maven/MavenAlgTest.scala | 4 +- .../core/forge/ForgeAuthAlgTest.scala | 79 ++++++ .../core/forge/ForgeRepoAlgTest.scala | 27 +- .../core/forge/ForgeRepoTest.scala | 15 +- .../core/forge/ForgeSelectionTest.scala | 35 --- .../core/forge/ForgeTypeTest.scala | 25 +- .../azurerepos/AzureReposApiAlgTest.scala | 46 ++-- .../forge/bitbucket/BitbucketApiAlgTest.scala | 32 +-- .../BitbucketServerApiAlgTest.scala | 42 +-- .../core/forge/gitea/GiteaApiAlgTest.scala | 34 +-- .../core/forge/github/GitHubApiAlgTest.scala | 36 ++- .../forge/github/GitHubAppApiAlgTest.scala | 154 +++++------ .../core/forge/github/GitHubAuthAlgTest.scala | 13 +- .../core/forge/gitlab/GitLabApiAlgTest.scala | 109 ++------ .../core/git/FileGitAlgTest.scala | 8 +- .../core/io/MockWorkspaceAlg.scala | 2 +- .../scalasteward/core/io/ProcessAlgTest.scala | 8 +- .../scalasteward/core/mock/GitHubAuth.scala | 19 ++ .../scalasteward/core/mock/MockConfig.scala | 42 +-- .../scalasteward/core/mock/MockContext.scala | 8 +- .../core/mock/MockForgeAuthAlg.scala | 14 + .../scalasteward/core/mock/MockState.scala | 4 +- .../core/nurture/NurtureAlgTest.scala | 10 +- .../nurture/PullRequestRepositoryTest.scala | 5 +- .../nurture/UpdateInfoUrlFinderTest.scala | 39 +-- .../persistence/JsonKeyValueStoreTest.scala | 16 +- .../core/repocache/RepoCacheAlgTest.scala | 30 +-- .../core/update/PruningAlgTest.scala | 22 +- 68 files changed, 1443 insertions(+), 1278 deletions(-) create mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/BasicAuthAlg.scala create mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/Forge.scala delete mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala delete mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/ForgeType.scala create mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerAuthAlg.scala delete mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApp.scala delete mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAppApiAlg.scala create mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabAuthAlg.scala create mode 100644 modules/core/src/test/scala/org/scalasteward/core/forge/ForgeAuthAlgTest.scala delete mode 100644 modules/core/src/test/scala/org/scalasteward/core/forge/ForgeSelectionTest.scala create mode 100644 modules/core/src/test/scala/org/scalasteward/core/mock/GitHubAuth.scala create mode 100644 modules/core/src/test/scala/org/scalasteward/core/mock/MockForgeAuthAlg.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala index 4964ed831a..67f23f09ce 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala @@ -30,13 +30,11 @@ import org.scalasteward.core.application.ExitCodePolicy.{ SuccessOnlyIfAllReposSucceed } import org.scalasteward.core.data.Resolver -import org.scalasteward.core.forge.ForgeType -import org.scalasteward.core.forge.ForgeType.{AzureRepos, GitHub} -import org.scalasteward.core.forge.github.GitHubApp +import org.scalasteward.core.forge.Forge +import org.scalasteward.core.forge.Forge._ import org.scalasteward.core.git.Author import org.scalasteward.core.util.Nel import org.scalasteward.core.util.dateTime.renderFiniteDuration - import scala.concurrent.duration._ object Cli { @@ -45,7 +43,6 @@ object Cli { object name { val forgeApiHost = "forge-api-host" val forgeLogin = "forge-login" - val forgeType = "forge-type" val maxBufferSize = "max-buffer-size" val processTimeout = "process-timeout" } @@ -71,11 +68,6 @@ object Cli { Validated.fromEither(Uri.fromString(s).leftMap(_.message)).toValidatedNel } - implicit val forgeTypeArgument: Argument[ForgeType] = - Argument.from(name.forgeType) { s => - Validated.fromEither(ForgeType.parse(s)).toValidatedNel - } - private val multiple = "(can be used multiple times)" private val workspace: Opts[File] = @@ -109,20 +101,7 @@ object Cli { flag("signoff", "Whether to signoff commits; default: false").orFalse private val gitCfg: Opts[GitCfg] = - (gitAuthor, gitAskPass, signCommits, signoff).mapN(GitCfg.apply) - - private val vcsType = - option[ForgeType]( - "vcs-type", - s"deprecated in favor of --${name.forgeType}", - visibility = Visibility.Partial - ).validate(s"--vcs-type is deprecated; use --${name.forgeType} instead")(_ => false) - - private val forgeType = { - val help = ForgeType.all.map(_.asString).mkString("One of ", ", ", "") + - s"; default: ${GitHub.asString}" - option[ForgeType](name.forgeType, help).orElse(vcsType).withDefault(GitHub) - } + (gitAuthor, signCommits, signoff).mapN(GitCfg.apply) private val vcsApiHost = option[Uri]( @@ -132,9 +111,7 @@ object Cli { ).validate(s"--vcs-api-host is deprecated; use --${name.forgeApiHost} instead")(_ => false) private val forgeApiHost: Opts[Uri] = - option[Uri](name.forgeApiHost, s"API URL of the forge; default: ${GitHub.publicApiBaseUrl}") - .orElse(vcsApiHost) - .withDefault(GitHub.publicApiBaseUrl) + option[Uri](name.forgeApiHost, s"API URL of the forge").orElse(vcsApiHost) private val vcsLogin = option[String]( @@ -155,16 +132,6 @@ object Cli { "Whether to add labels on pull or merge requests (if supported by the forge)" ).orFalse - private val forgeCfg: Opts[ForgeCfg] = - (forgeType, forgeApiHost, forgeLogin, doNotFork, addPrLabels) - .mapN(ForgeCfg.apply) - .validate( - s"${ForgeType.allNot(_.supportsForking)} do not support fork mode" - )(cfg => cfg.tpe.supportsForking || cfg.doNotFork) - .validate( - s"${ForgeType.allNot(_.supportsLabels)} do not support pull request labels" - )(cfg => cfg.tpe.supportsLabels || !cfg.addLabels) - private val ignoreOptsFiles: Opts[Boolean] = flag( "ignore-opts-files", @@ -195,11 +162,11 @@ object Cli { s"Read only directory for the sandbox $multiple" ).orEmpty - private val enableSandbox: Opts[Boolean] = - flag("enable-sandbox", "Whether to use the sandbox") - .map(_ => true) - .orElse(flag("disable-sandbox", "Whether to not use the sandbox").map(_ => false)) - .orElse(Opts(false)) + private val enableSandbox: Opts[Boolean] = { + val enable = flag("enable-sandbox", "Whether to use the sandbox").map(_ => true) + val disable = flag("disable-sandbox", "Whether to not use the sandbox").map(_ => false) + enable.orElse(disable).withDefault(false) + } private val sandboxCfg: Opts[SandboxCfg] = (whitelist, readOnly, enableSandbox).mapN(SandboxCfg.apply) @@ -271,12 +238,6 @@ object Cli { "Whether to assign the default reviewers to a bitbucket pull request; default: false" ).orFalse - private val bitbucketServerCfg: Opts[BitbucketServerCfg] = - bitbucketServerUseDefaultReviewers.map(BitbucketServerCfg.apply) - - private val bitbucketCfg: Opts[BitbucketCfg] = - bitbucketUseDefaultReviewers.map(BitbucketCfg.apply) - private val gitlabMergeWhenPipelineSucceeds: Opts[Boolean] = flag( "gitlab-merge-when-pipeline-succeeds", @@ -295,11 +256,6 @@ object Cli { "Flag indicating if a merge request should remove the source branch when merging." ).orFalse - private val gitLabCfg: Opts[GitLabCfg] = - (gitlabMergeWhenPipelineSucceeds, gitlabRequiredReviewers, gitlabRemoveSourceBranch).mapN( - GitLabCfg.apply - ) - private val githubAppId: Opts[Long] = option[Long]( "github-app-id", @@ -312,17 +268,11 @@ object Cli { "GitHub application key file. Repos accessible by this app are added to the repos in repos.md. git-ask-pass is still required." ) - private val gitHubApp: Opts[Option[GitHubApp]] = - (githubAppId, githubAppKeyFile).mapN(GitHubApp.apply).orNone - - private val azureReposOrganization: Opts[Option[String]] = + private val azureReposOrganization: Opts[String] = option[String]( "azure-repos-organization", - s"The Azure organization (required when --${name.forgeType} is ${AzureRepos.asString})" - ).orNone - - private val azureReposCfg: Opts[AzureReposCfg] = - azureReposOrganization.map(AzureReposCfg.apply) + s"The Azure organization (required with --azure-repos)" + ) private val refreshBackoffPeriod: Opts[FiniteDuration] = { val default = 0.days @@ -353,22 +303,120 @@ object Cli { if (ifAnyRepoSucceeds) SuccessIfAnyRepoSucceeds else SuccessOnlyIfAllReposSucceed } + private val azureRepos: Opts[Unit] = + flag("azure-repos", "") + + private val bitbucket: Opts[Unit] = + flag("bitbucket", "") + + private val bitbucketServer: Opts[Unit] = + flag("bitbucket-server", "") + + private val gitLab: Opts[Unit] = + flag("gitlab", "") + + private val gitea: Opts[Unit] = + flag("gitea", "") + + private val gitHub: Opts[Unit] = + flag("github", "").withDefault(()) // With default to make it succeed as default option + + private val forge: Opts[Forge] = { + val azureReposOptions = + (azureRepos, forgeApiHost, forgeLogin, gitAskPass, addPrLabels, azureReposOrganization).mapN( + (_, apiUri, login, gitAskPass, addLabels, reposOrganization) => + AzureRepos(apiUri, login, gitAskPass, addLabels, reposOrganization) + ) + val bitbucketOptions = + ( + bitbucket, + forgeApiHost.withDefault(Bitbucket.defaultApiUri), + forgeLogin, + gitAskPass, + doNotFork, + bitbucketUseDefaultReviewers + ).mapN((_, apiUri, login, gitAskPass, doNotFork, useDefaultReviewers) => + Bitbucket(apiUri, login, gitAskPass, doNotFork, useDefaultReviewers) + ) + val bitbucketServerOptions = + ( + bitbucketServer, + forgeApiHost, + forgeLogin, + gitAskPass, + bitbucketServerUseDefaultReviewers + ).mapN((_, apiUri, login, gitAskPass, useDefaultReviewers) => + BitbucketServer(apiUri, login, gitAskPass, useDefaultReviewers) + ) + val gitLabOptions = + ( + gitLab, + forgeApiHost.withDefault(GitLab.defaultApiUri), + forgeLogin, + gitAskPass, + doNotFork, + addPrLabels, + gitlabMergeWhenPipelineSucceeds, + gitlabRequiredReviewers, + gitlabRemoveSourceBranch + ).mapN( + ( + _, + apiUri, + login, + gitAskPass, + doNotFork, + addLabels, + mergeWhenPipelineSucceeds, + requiredReviewers, + removeSourceBranch + ) => + GitLab( + apiUri, + login, + gitAskPass, + doNotFork, + addLabels, + mergeWhenPipelineSucceeds, + requiredReviewers, + removeSourceBranch + ) + ) + val giteaOptions = + (gitea, forgeApiHost, forgeLogin, gitAskPass, doNotFork, addPrLabels).mapN( + (_, apiUri, login, gitAskPass, doNotFork, addLabels) => + Gitea(apiUri, login, gitAskPass, doNotFork, addLabels) + ) + val gitHubOptions = + ( + gitHub, + forgeApiHost.withDefault(GitHub.defaultApiUri), + doNotFork, + addPrLabels, + githubAppId, + githubAppKeyFile + ).mapN((_, apiUri, doNotFork, addLabels, appId, appKeyFile) => + GitHub(apiUri, doNotFork, addLabels, appId, appKeyFile) + ) + azureReposOptions + .orElse(bitbucketOptions) + .orElse(bitbucketServerOptions) + .orElse(gitLabOptions) + .orElse(giteaOptions) + .orElse(gitHubOptions) // GitHub last as default option + } + private val regular: Opts[Usage] = ( workspace, reposFiles, gitCfg, - forgeCfg, + forge, ignoreOptsFiles, processCfg, repoConfigCfg, scalafixCfg, artifactCfg, cacheTtl, - bitbucketCfg, - bitbucketServerCfg, - gitLabCfg, - azureReposCfg, - gitHubApp, urlCheckerTestUrls, defaultMavenRepo, refreshBackoffPeriod, diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala index 4e836627af..b22c374b99 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala @@ -21,8 +21,7 @@ import org.http4s.Uri import org.scalasteward.core.application.Cli.EnvVar import org.scalasteward.core.application.Config._ import org.scalasteward.core.data.Resolver -import org.scalasteward.core.forge.ForgeType -import org.scalasteward.core.forge.github.GitHubApp +import org.scalasteward.core.forge.Forge import org.scalasteward.core.git.Author import org.scalasteward.core.util.Nel import scala.concurrent.duration.FiniteDuration @@ -49,50 +48,26 @@ final case class Config( workspace: File, reposFiles: Nel[Uri], gitCfg: GitCfg, - forgeCfg: ForgeCfg, + forge: Forge, ignoreOptsFiles: Boolean, processCfg: ProcessCfg, repoConfigCfg: RepoConfigCfg, scalafixCfg: ScalafixCfg, artifactCfg: ArtifactCfg, cacheTtl: FiniteDuration, - bitbucketCfg: BitbucketCfg, - bitbucketServerCfg: BitbucketServerCfg, - gitLabCfg: GitLabCfg, - azureReposCfg: AzureReposCfg, - githubApp: Option[GitHubApp], urlCheckerTestUrls: Nel[Uri], defaultResolver: Resolver, refreshBackoffPeriod: FiniteDuration, exitCodePolicy: ExitCodePolicy -) { - def forgeSpecificCfg: ForgeSpecificCfg = - forgeCfg.tpe match { - case ForgeType.AzureRepos => azureReposCfg - case ForgeType.Bitbucket => bitbucketCfg - case ForgeType.BitbucketServer => bitbucketServerCfg - case ForgeType.GitHub => GitHubCfg() - case ForgeType.GitLab => gitLabCfg - case ForgeType.Gitea => GiteaCfg() - } -} +) object Config { final case class GitCfg( gitAuthor: Author, - gitAskPass: File, signCommits: Boolean, signoff: Boolean ) - final case class ForgeCfg( - tpe: ForgeType, - apiHost: Uri, - login: String, - doNotFork: Boolean, - addLabels: Boolean - ) - final case class ProcessCfg( envVars: List[EnvVar], processTimeout: FiniteDuration, @@ -120,30 +95,4 @@ object Config { migrations: List[Uri], disableDefaults: Boolean ) - - sealed trait ForgeSpecificCfg extends Product with Serializable - - final case class AzureReposCfg( - organization: Option[String] - ) extends ForgeSpecificCfg - - final case class BitbucketCfg( - useDefaultReviewers: Boolean - ) extends ForgeSpecificCfg - - final case class BitbucketServerCfg( - useDefaultReviewers: Boolean - ) extends ForgeSpecificCfg - - final case class GitHubCfg( - ) extends ForgeSpecificCfg - - final case class GitLabCfg( - mergeWhenPipelineSucceeds: Boolean, - requiredReviewers: Option[Int], - removeSourceBranch: Boolean - ) extends ForgeSpecificCfg - - final case class GiteaCfg( - ) extends ForgeSpecificCfg } 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..f1c8036cc7 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 @@ -23,7 +23,6 @@ import eu.timepit.refined.auto._ import org.http4s.Uri import org.http4s.client.Client import org.http4s.headers.`User-Agent` -import org.scalasteward.core.application.Config.ForgeCfg import org.scalasteward.core.buildtool.BuildToolDispatcher import org.scalasteward.core.buildtool.maven.MavenAlg import org.scalasteward.core.buildtool.mill.MillAlg @@ -36,8 +35,7 @@ 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.forge.{ForgeApiAlg, ForgeAuthAlg, ForgeRepoAlg} import org.scalasteward.core.git.{GenGitAlg, GitAlg} import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} import org.scalasteward.core.nurture.{NurtureAlg, PullRequestRepository, UpdateInfoUrlFinder} @@ -62,6 +60,7 @@ final class Context[F[_]](implicit val filterAlg: FilterAlg[F], val forgeRepoAlg: ForgeRepoAlg[F], val gitAlg: GitAlg[F], + val forgeAuthAlg: ForgeAuthAlg[F], val hookExecutor: HookExecutor[F], val httpJsonClient: HttpJsonClient[F], val logger: Logger[F], @@ -131,16 +130,13 @@ object Context { ): F[Context[F]] = 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)) - kvsPrefix = Some(config.forgeCfg.tpe.asString) + kvsPrefix = Some(config.forge.toString) pullRequestsStore <- JsonKeyValueStore .create[F, Repo, Map[Uri, PullRequestRepository.Entry]]("pull_requests", "2", kvsPrefix) .flatMap(CachingKeyValueStore.wrap(_)) @@ -155,21 +151,20 @@ object Context { implicit val artifactMigrationsFinder: ArtifactMigrationsFinder = artifactMigrationsFinder0 implicit val scalafixMigrationsLoader: ScalafixMigrationsLoader[F] = scalafixMigrationsLoader0 implicit val scalafixMigrationsFinder: ScalafixMigrationsFinder = scalafixMigrationsFinder0 - implicit val urlChecker: UrlChecker[F] = urlChecker0 + implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F] + implicit val forgeAuthAlg: ForgeAuthAlg[F] = ForgeAuthAlg.create[F](config.forge) + implicit val urlChecker: UrlChecker[F] = UrlChecker.create[F](config) 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] = ForgeApiAlg.create[F](config.forge) implicit val forgeRepoAlg: ForgeRepoAlg[F] = new ForgeRepoAlg[F](config) - implicit val forgeCfg: ForgeCfg = config.forgeCfg - implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F] + implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = + new UpdateInfoUrlFinder[F](config.forge) implicit val pullRequestRepository: PullRequestRepository[F] = new PullRequestRepository[F](pullRequestsStore) implicit val scalafixCli: ScalafixCli[F] = new ScalafixCli[F] @@ -189,11 +184,9 @@ object Context { implicit val repoCacheAlg: RepoCacheAlg[F] = new RepoCacheAlg[F](config) implicit val scannerAlg: ScannerAlg[F] = new ScannerAlg[F] implicit val editAlg: EditAlg[F] = new EditAlg[F] - implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config.forgeCfg) + implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config.forge) 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] } diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/ExitCodePolicy.scala b/modules/core/src/main/scala/org/scalasteward/core/application/ExitCodePolicy.scala index 7150b58a93..ba2c5483c0 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/ExitCodePolicy.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/ExitCodePolicy.scala @@ -23,7 +23,7 @@ trait ExitCodePolicy { } object ExitCodePolicy { - def successIf(isSuccess: RunResults => Boolean): ExitCodePolicy = + private def successIf(isSuccess: RunResults => Boolean): ExitCodePolicy = (runResults: RunResults) => if (isSuccess(runResults)) ExitCode.Success else ExitCode.Error val SuccessIfAnyRepoSucceeds: ExitCodePolicy = successIf(_.successRepos.nonEmpty) 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/coursier/DependencyMetadata.scala b/modules/core/src/main/scala/org/scalasteward/core/coursier/DependencyMetadata.scala index 1495c57704..efacb0d8e6 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/coursier/DependencyMetadata.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/coursier/DependencyMetadata.scala @@ -19,8 +19,7 @@ package org.scalasteward.core.coursier import cats.Monad import cats.syntax.all._ import org.http4s.Uri -import org.scalasteward.core.application.Config.ForgeCfg -import org.scalasteward.core.forge.ForgeRepo +import org.scalasteward.core.forge.{Forge, ForgeRepo} import org.scalasteward.core.util.uri final case class DependencyMetadata( @@ -49,8 +48,8 @@ final case class DependencyMetadata( urls.find(_.scheme.exists(uri.httpSchemes)).orElse(urls.headOption) } - def forgeRepo(implicit config: ForgeCfg): Option[ForgeRepo] = - repoUrl.flatMap(ForgeRepo.fromRepoUrl) + def forgeRepo(forge: Forge): Option[ForgeRepo] = + repoUrl.flatMap(ForgeRepo.fromRepoUrl(_, forge)) } object DependencyMetadata { diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala index 22c005d6a0..9300583f34 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala @@ -113,7 +113,7 @@ final case class Version(value: String) { case _ => false } || Rfc5234.hexdig.rep(8).string.filterNot(startsWithDate).parse(value).isRight - private[this] def alnumComponentsWithoutPreRelease: List[Version.Component] = + private def alnumComponentsWithoutPreRelease: List[Version.Component] = alnumComponents.takeWhile { case a: Version.Component.Alpha => !a.isPreReleaseIdent case _ => true 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/Forge.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/Forge.scala new file mode 100644 index 0000000000..ff9ada271c --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/Forge.scala @@ -0,0 +1,129 @@ +/* + * 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.Eq +import org.http4s.Uri +import org.http4s.syntax.literals._ +import org.scalasteward.core.data.Repo +import org.scalasteward.core.git.Branch +import scala.annotation.nowarn + +sealed trait Forge extends Product with Serializable { + def apiUri: Uri + def doNotFork: Boolean + def addLabels: Boolean + + /** Determines the `head` (GitHub) / `source_branch` (GitLab, Bitbucket) parameter for searching + * for already existing pull requests or creating new pull requests. + */ + def pullRequestHeadFor(@nowarn fork: Repo, updateBranch: Branch): String = updateBranch.name +} + +object Forge { + case class AzureRepos( + apiUri: Uri, + login: String, + gitAskPass: File, + addLabels: Boolean, + reposOrganization: String + ) extends Forge { + + /** Azure Repos does not support forking */ + val doNotFork: Boolean = true + override val toString: String = "azure-repos" + } + + case class Bitbucket( + apiUri: Uri, + login: String, + gitAskPass: File, + doNotFork: Boolean, + useDefaultReviewers: Boolean + ) extends Forge { + + /** Bitbucket does not support labels on PRs */ + val addLabels: Boolean = false + override val toString: String = "bitbucket" + } + object Bitbucket { + val defaultApiUri: Uri = uri"https://api.bitbucket.org/2.0" + } + + /** Note Bitbucket Server will be End Of Service Life on 15th February 2024: + * + * https://www.atlassian.com/software/bitbucket/enterprise + * https://www.atlassian.com/migration/assess/journey-to-cloud + */ + case class BitbucketServer( + apiUri: Uri, + login: String, + gitAskPass: File, + useDefaultReviewers: Boolean + ) extends Forge { + + /** Bitbucket Server does not support forking */ + val doNotFork: Boolean = true + + /** Bitbucket Server does not support labels on PRs */ + val addLabels: Boolean = false + override val toString: String = "bitbucket-server" + } + + case class GitHub( + apiUri: Uri, + doNotFork: Boolean, + addLabels: Boolean, + appId: Long, + appKeyFile: File + ) extends Forge { + override def pullRequestHeadFor(fork: Repo, updateBranch: Branch): String = + s"${fork.owner}:${updateBranch.name}" + override val toString: String = "github" + } + object GitHub { + val defaultApiUri: Uri = uri"https://api.github.com" + } + + case class GitLab( + apiUri: Uri, + login: String, + gitAskPass: File, + doNotFork: Boolean, + addLabels: Boolean, + mergeWhenPipelineSucceeds: Boolean, + requiredReviewers: Option[Int], + removeSourceBranch: Boolean + ) extends Forge + object GitLab { + val defaultApiUri: Uri = uri"https://gitlab.com/api/v4" + override val toString: String = "gitlab" + } + + case class Gitea( + apiUri: Uri, + login: String, + gitAskPass: File, + doNotFork: Boolean, + addLabels: Boolean + ) extends Forge { + override val toString: String = "gitea" + } + + implicit val forgeEq: Eq[Forge] = Eq.fromUniversalEquals +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeApiAlg.scala index 99479cd9b4..09c030213d 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeApiAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeApiAlg.scala @@ -16,11 +16,28 @@ package org.scalasteward.core.forge +import cats.effect.Temporal import cats.syntax.all._ -import cats.{ApplicativeThrow, MonadThrow} +import cats.{ApplicativeThrow, MonadThrow, Parallel} import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.{ + AzureRepos, + Bitbucket, + BitbucketServer, + GitHub, + GitLab, + Gitea +} +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._ +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.git.Branch +import org.scalasteward.core.util.HttpJsonClient +import org.typelevel.log4cats.Logger trait ForgeApiAlg[F[_]] { def createFork(repo: Repo): F[RepoOut] @@ -67,3 +84,22 @@ trait ForgeApiAlg[F[_]] { private def getDefaultBranch(repoOut: RepoOut): F[BranchOut] = getBranch(repoOut.repo, repoOut.default_branch) } + +object ForgeApiAlg { + def create[F[_]: Parallel](forge: Forge)(implicit + httpJsonClient: HttpJsonClient[F], + forgeAuthAlg: ForgeAuthAlg[F], + logger: Logger[F], + F: Temporal[F] + ): ForgeApiAlg[F] = { + val auth = forgeAuthAlg.authenticateApi(_) + forge match { + case forge: AzureRepos => new AzureReposApiAlg(forge, auth) + case forge: Bitbucket => new BitbucketApiAlg(forge, auth) + case forge: BitbucketServer => new BitbucketServerApiAlg(forge, auth) + case forge: GitHub => new GitHubApiAlg(forge, auth) + case forge: GitLab => new GitLabApiAlg(forge, auth) + case forge: Gitea => new GiteaApiAlg(forge, auth) + } + } +} 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..a232f16fdd 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,47 @@ 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.data.Repo +import org.scalasteward.core.forge.Forge.{ + AzureRepos, + Bitbucket, + BitbucketServer, + GitHub, + GitLab, + Gitea +} +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 + +trait ForgeAuthAlg[F[_]] { + def authenticateApi(req: Request[F]): F[Request[F]] + def authenticateGit(uri: Uri): F[Uri] + def accessibleRepos: F[List[Repo]] +} -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) +object ForgeAuthAlg { + def create[F[_]](forge: Forge)(implicit + F: Sync[F], + client: HttpJsonClient[F], + workspaceAlg: WorkspaceAlg[F], + processAlg: ProcessAlg[F], + logger: Logger[F] + ): ForgeAuthAlg[F] = + forge match { + case forge: AzureRepos => + new BasicAuthAlg(forge.apiUri, forge.login, forge.gitAskPass) + case forge: Bitbucket => + new BasicAuthAlg(forge.apiUri, forge.login, forge.gitAskPass) + case forge: BitbucketServer => + new BitbucketServerAuthAlg(forge.apiUri, forge.login, forge.gitAskPass) + case forge: GitHub => new GitHubAuthAlg(forge.apiUri, forge.appId, forge.appKeyFile) + case forge: GitLab => new GitLabAuthAlg(forge.apiUri, forge.login, forge.gitAskPass) + case forge: Gitea => new BasicAuthAlg(forge.apiUri, forge.login, forge.gitAskPass) + } } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepo.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepo.scala index ee0c8ff711..28704a9a8b 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepo.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepo.scala @@ -17,21 +17,82 @@ package org.scalasteward.core.forge import org.http4s.Uri -import org.scalasteward.core.application.Config.ForgeCfg /** ForgeRepo encapsulates two concepts that are commonly considered together - the URI of a repo, * and the 'type' of forge that url represents. Given a URI, once we know it's a GitHub or GitLab * forge, etc, then we can know how to construct many of the urls for common resources existing at * that repo host- for instance, the url to view a particular file, or to diff two commits. */ -case class ForgeRepo(forgeType: ForgeType, repoUrl: Uri) { - def diffUrlFor(from: String, to: String): Uri = forgeType.diffs.forDiff(from, to)(repoUrl) +trait ForgeRepo { - def fileUrlFor(fileName: String): Uri = forgeType.files.forFile(fileName)(repoUrl) + /** Defines how to construct 'diff' urls for this forge type - ie a url that will show the + * difference between two git tags. These can be very useful for understanding the difference + * between two releases of the same artifact. + */ + def diffUrlFor(from: String, to: String): Uri + + /** Defines how to construct 'file' urls for this forge type - ie a url that will display a + * specific file's contents. This is useful for linking to Release Notes, etc, in a Scala Steward + * PR description. + */ + def fileUrlFor(fileName: String): Uri } object ForgeRepo { - def fromRepoUrl(repoUrl: Uri)(implicit config: ForgeCfg): Option[ForgeRepo] = for { - repoForgeType <- ForgeType.fromRepoUrl(repoUrl) - } yield ForgeRepo(repoForgeType, repoUrl) + case class AzureRepos(repoUrl: Uri) extends ForgeRepo { + override def diffUrlFor(from: String, to: String): Uri = + repoUrl / "branchCompare" +? ("baseVersion", s"GT$from") +? ("targetVersion", s"GT$to") + override def fileUrlFor(fileName: String): Uri = repoUrl.withQueryParam( + "path", + fileName + ) // Azure's canonical value for the path is prefixed with a slash? + } + + case class Bitbucket(repoUrl: Uri) extends ForgeRepo { + override def diffUrlFor(from: String, to: String): Uri = + (repoUrl / "compare" / s"$to..$from").withFragment("diff") + override def fileUrlFor(fileName: String): Uri = repoUrl / "src" / "master" / fileName + } + + case class BitbucketServer(repoUrl: Uri) extends ForgeRepo { + override def diffUrlFor(from: String, to: String): Uri = + (repoUrl / "compare" / s"$to..$from").withFragment("diff") + override def fileUrlFor(fileName: String): Uri = repoUrl / "browse" / fileName + } + + case class GitHub(repoUrl: Uri) extends ForgeRepo { + override def diffUrlFor(from: String, to: String): Uri = repoUrl / "compare" / s"$from...$to" + override def fileUrlFor(fileName: String): Uri = repoUrl / "blob" / "master" / fileName + } + + case class GitLab(repoUrl: Uri) extends ForgeRepo { + override def diffUrlFor(from: String, to: String): Uri = repoUrl / "compare" / s"$from...$to" + override def fileUrlFor(fileName: String): Uri = repoUrl / "blob" / "master" / fileName + } + + case class Gitea(repoUrl: Uri) extends ForgeRepo { + override def diffUrlFor(from: String, to: String): Uri = repoUrl / "compare" / s"$from...$to" + override def fileUrlFor(fileName: String): Uri = + repoUrl / "src" / "branch" / "master" / fileName + } + + def fromRepoUrl(repoUrl: Uri, forge: Forge): Option[ForgeRepo] = + repoUrl.host.flatMap { repoHost => + Option + .when(forge.apiUri.host.contains(repoHost))(forge match { + case _: Forge.AzureRepos => AzureRepos(repoUrl) + case _: Forge.Bitbucket => Bitbucket(repoUrl) + case _: Forge.BitbucketServer => BitbucketServer(repoUrl) + case _: Forge.GitHub => GitHub(repoUrl) + case _: Forge.GitLab => GitLab(repoUrl) + case _: Forge.Gitea => Gitea(repoUrl) + }) + .orElse(repoHost.value match { + case "dev.azure.com" => Some(AzureRepos(repoUrl)) + case "api.bitbucket.org" => Some(Bitbucket(repoUrl)) + case "api.github.com" => Some(GitHub(repoUrl)) + case "gitlab.com" => Some(GitLab(repoUrl)) + case _ => None + }) + } } 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..492f532212 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,32 +18,35 @@ 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.Forge.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 => + case throwable if (config.forge match { + case gitHub: GitHub => !gitHub.doNotFork + case _ => false + }) => val message = """|If cloning failed with an error like 'access denied or repository not exported' |the fork might not be ready yet. This error might disappear on the next run. @@ -53,15 +56,16 @@ final class ForgeRepoAlg[F[_]](config: Config)(implicit } private def maybeCheckoutBranchOrSyncFork(repo: Repo, repoOut: RepoOut): F[Unit] = - if (config.forgeCfg.doNotFork) repo.branch.fold(F.unit)(gitAlg.checkoutBranch(repo, _)) + if (config.forge.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 +81,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 deleted file mode 100644 index 9fb7c0ca1f..0000000000 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 cats.effect.Temporal -import cats.syntax.all._ -import cats.{Applicative, Functor, Parallel} -import org.http4s.headers.Authorization -import org.http4s.{BasicCredentials, Header, 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] - )(implicit - httpJsonClient: HttpJsonClient[F], - logger: Logger[F], - F: Temporal[F] - ): ForgeApiAlg[F] = { - val auth = authenticate(forgeCfg.tpe, user) - forgeSpecificCfg match { - case specificCfg: Config.AzureReposCfg => - new AzureReposApiAlg(forgeCfg.apiHost, specificCfg, auth) - case specificCfg: Config.BitbucketCfg => - new BitbucketApiAlg(forgeCfg, specificCfg, auth) - case specificCfg: Config.BitbucketServerCfg => - new BitbucketServerApiAlg(forgeCfg.apiHost, specificCfg, auth) - case _: Config.GitHubCfg => - new GitHubApiAlg(forgeCfg.apiHost, auth) - case specificCfg: Config.GitLabCfg => - new GitLabApiAlg(forgeCfg, specificCfg, auth) - 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/ForgeType.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeType.scala deleted file mode 100644 index 4d14949253..0000000000 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeType.scala +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 cats.Eq -import cats.syntax.all._ -import org.http4s.Uri -import org.http4s.syntax.literals._ -import org.scalasteward.core.application.Config.ForgeCfg -import org.scalasteward.core.data.Repo -import org.scalasteward.core.forge.ForgeType._ -import org.scalasteward.core.git.Branch -import org.scalasteward.core.util.unexpectedString - -import scala.annotation.nowarn - -sealed trait ForgeType extends Product with Serializable { - def publicWebHost: Option[String] - - /** Defines how to construct 'diff' urls for this forge type - ie a url that will show the - * difference between two git tags. These can be very useful for understanding the difference - * between two releases of the same artifact. - */ - val diffs: DiffUriPattern - - /** Defines how to construct 'file' urls for this forge type - ie a url that will display a - * specific file's contents. This is useful for linking to Release Notes, etc, in a Scala Steward - * PR description. - */ - val files: FileUriPattern - def supportsForking: Boolean = true - def supportsLabels: Boolean = true - - /** Determines the `head` (GitHub) / `source_branch` (GitLab, Bitbucket) parameter for searching - * for already existing pull requests or creating new pull requests. - */ - def pullRequestHeadFor(@nowarn fork: Repo, updateBranch: Branch): String = updateBranch.name - - val asString: String = this match { - case AzureRepos => "azure-repos" - case Bitbucket => "bitbucket" - case BitbucketServer => "bitbucket-server" - case GitHub => "github" - case GitLab => "gitlab" - case Gitea => "gitea" - } -} - -object ForgeType { - trait DiffUriPattern { def forDiff(from: String, to: String): Uri => Uri } - trait FileUriPattern { def forFile(fileName: String): Uri => Uri } - - case object AzureRepos extends ForgeType { - override val publicWebHost: Some[String] = Some("dev.azure.com") - override def supportsForking: Boolean = false - val diffs: DiffUriPattern = (from, to) => - _ / "branchCompare" +? ("baseVersion", s"GT$from") +? ("targetVersion", s"GT$to") - val files: FileUriPattern = - fileName => - _.withQueryParam( - "path", - fileName - ) // Azure's canonical value for the path is prefixed with a slash? - } - - case object Bitbucket extends ForgeType { - override val publicWebHost: Some[String] = Some("bitbucket.org") - override def supportsLabels: Boolean = false - val publicApiBaseUrl = uri"https://api.bitbucket.org/2.0" - val diffs: DiffUriPattern = (from, to) => _ / "compare" / s"$to..$from" withFragment "diff" - val files: FileUriPattern = fileName => _ / "src" / "master" / fileName - } - - /** Note Bitbucket Server will be End Of Service Life on 15th February 2024: - * - * https://www.atlassian.com/software/bitbucket/enterprise - * https://www.atlassian.com/migration/assess/journey-to-cloud - */ - case object BitbucketServer extends ForgeType { - override val publicWebHost: None.type = None - override def supportsForking: Boolean = false - override def supportsLabels: Boolean = false - val diffs: DiffUriPattern = Bitbucket.diffs - val files: FileUriPattern = fileName => _ / "browse" / fileName - } - - case object GitHub extends ForgeType { - override val publicWebHost: Some[String] = Some("github.com") - val publicApiBaseUrl = uri"https://api.github.com" - val diffs: DiffUriPattern = (from, to) => _ / "compare" / s"$from...$to" - val files: FileUriPattern = fileName => _ / "blob" / "master" / fileName - override def pullRequestHeadFor(fork: Repo, updateBranch: Branch): String = - s"${fork.owner}:${updateBranch.name}" - } - - case object GitLab extends ForgeType { - override val publicWebHost: Some[String] = Some("gitlab.com") - val publicApiBaseUrl = uri"https://gitlab.com/api/v4" - val diffs: DiffUriPattern = GitHub.diffs - val files: FileUriPattern = GitHub.files - } - - case object Gitea extends ForgeType { - override val publicWebHost: Option[String] = None - val diffs: DiffUriPattern = GitHub.diffs - val files: FileUriPattern = fileName => _ / "src" / "branch" / "master" / fileName - } - - val all: List[ForgeType] = List(AzureRepos, Bitbucket, BitbucketServer, GitHub, GitLab, Gitea) - - def allNot(f: ForgeType => Boolean): String = - ForgeType.all.filterNot(f).map(_.asString).mkString(", ") - - def parse(s: String): Either[String, ForgeType] = - all.find(_.asString === s) match { - case Some(value) => Right(value) - case None => unexpectedString(s, all.map(_.asString)) - } - - def fromPublicWebHost(host: String): Option[ForgeType] = - all.find(_.publicWebHost.contains_(host)) - - /** Attempts to guess, based on the uri host and the config used to launch Scala Steward, what - * type of forge hosts the repo at the supplied uri. - */ - def fromRepoUrl(repoUrl: Uri)(implicit config: ForgeCfg): Option[ForgeType] = - repoUrl.host.flatMap { repoHost => - Option - .when(config.apiHost.host.contains(repoHost))(config.tpe) - .orElse(fromPublicWebHost(repoHost.value)) - } - - implicit val forgeTypeEq: Eq[ForgeType] = - Eq.fromUniversalEquals -} diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala index 4dd5698910..fe1b70dd34 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala @@ -18,9 +18,9 @@ package org.scalasteward.core.forge.azurerepos import cats.MonadThrow import cats.syntax.all._ -import org.http4s.{Request, Uri} -import org.scalasteward.core.application.Config.AzureReposCfg +import org.http4s.Request import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.AzureRepos import org.scalasteward.core.forge.ForgeApiAlg import org.scalasteward.core.forge.azurerepos.JsonCodec._ import org.scalasteward.core.forge.data._ @@ -29,13 +29,12 @@ import org.scalasteward.core.util.HttpJsonClient import org.typelevel.log4cats.Logger final class AzureReposApiAlg[F[_]]( - azureAPiHost: Uri, - config: AzureReposCfg, + forge: AzureRepos, modify: Request[F] => F[Request[F]] )(implicit client: HttpJsonClient[F], logger: Logger[F], F: MonadThrow[F]) extends ForgeApiAlg[F] { - private val url = new Url(azureAPiHost, config.organization.getOrElse("")) + private val url = new Url(forge.apiUri, forge.reposOrganization) override def createFork(repo: Repo): F[RepoOut] = F.raiseError(new NotImplementedError(s"createFork($repo)")) diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala index 65af40706e..529a30010d 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala @@ -19,8 +19,8 @@ package org.scalasteward.core.forge.bitbucket import cats.MonadThrow import cats.syntax.all._ import org.http4s.{Request, Status} -import org.scalasteward.core.application.Config.{BitbucketCfg, ForgeCfg} import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.Bitbucket import org.scalasteward.core.forge.ForgeApiAlg import org.scalasteward.core.forge.bitbucket.json._ import org.scalasteward.core.forge.data._ @@ -30,21 +30,20 @@ import org.typelevel.log4cats.Logger /** https://developer.atlassian.com/bitbucket/api/2/reference/ */ class BitbucketApiAlg[F[_]]( - config: ForgeCfg, - bitbucketCfg: BitbucketCfg, + forge: Bitbucket, modify: Request[F] => F[Request[F]] )(implicit client: HttpJsonClient[F], logger: Logger[F], F: MonadThrow[F] ) extends ForgeApiAlg[F] { - private val url = new Url(config.apiHost) + private val url = new Url(forge.apiUri) override def createFork(repo: Repo): F[RepoOut] = for { fork <- client.post[RepositoryResponse](url.forks(repo), modify).recoverWith { case UnexpectedResponse(_, _, _, Status.BadRequest, _) => - client.get(url.repo(repo.copy(owner = config.login)), modify) + client.get(url.repo(repo.copy(owner = forge.login)), modify) } maybeParent <- fork.parent @@ -68,9 +67,9 @@ class BitbucketApiAlg[F[_]]( client.get[DefaultReviewers](url.defaultReviewers(repo), modify).map(_.values) override def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut] = { - val sourceBranchOwner = if (config.doNotFork) repo.owner else config.login + val sourceBranchOwner = if (forge.doNotFork) repo.owner else forge.login val defaultReviewers = - if (bitbucketCfg.useDefaultReviewers) getDefaultReviewers(repo) + if (forge.useDefaultReviewers) getDefaultReviewers(repo) else F.pure(List.empty[Reviewer]) val create: F[PullRequestOut] = diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala index 71cac81c6c..5a87fd88f4 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala @@ -18,9 +18,9 @@ package org.scalasteward.core.forge.bitbucketserver import cats.MonadThrow import cats.syntax.all._ -import org.http4s.{Request, Uri} -import org.scalasteward.core.application.Config.BitbucketServerCfg +import org.http4s.Request import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.BitbucketServer import org.scalasteward.core.forge.ForgeApiAlg import org.scalasteward.core.forge.bitbucketserver.Json.{PR, Reviewer, User} import org.scalasteward.core.forge.data.PullRequestState.Open @@ -31,12 +31,11 @@ import org.typelevel.log4cats.Logger /** https://docs.atlassian.com/bitbucket-server/rest/latest/bitbucket-rest.html */ final class BitbucketServerApiAlg[F[_]]( - bitbucketApiHost: Uri, - config: BitbucketServerCfg, + forge: BitbucketServer, modify: Request[F] => F[Request[F]] )(implicit client: HttpJsonClient[F], logger: Logger[F], F: MonadThrow[F]) extends ForgeApiAlg[F] { - private val url = new Url(bitbucketApiHost) + private val url = new Url(forge.apiUri) override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] = getPullRequest(repo, number).flatMap { pr => @@ -94,7 +93,7 @@ final class BitbucketServerApiAlg[F[_]]( logger.warn("Updating PRs is not yet supported for Bitbucket Server") private def useDefaultReviewers(repo: Repo): F[List[Reviewer]] = - if (config.useDefaultReviewers) getDefaultReviewers(repo) else F.pure(List.empty[Reviewer]) + if (forge.useDefaultReviewers) getDefaultReviewers(repo) else F.pure(List.empty[Reviewer]) private def declinePullRequest(repo: Repo, number: PullRequestNumber, version: Int): F[Unit] = client.post_(url.declinePullRequest(repo, number, version), modify) 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/gitea/GiteaApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala index 2364afd069..1a7ab85be3 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala @@ -21,8 +21,8 @@ import cats.implicits._ import io.circe._ import io.circe.generic.semiauto.{deriveCodec, deriveEncoder} import org.http4s.{Request, Uri} -import org.scalasteward.core.application.Config.ForgeCfg import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.Gitea import org.scalasteward.core.forge.ForgeApiAlg import org.scalasteward.core.forge.data._ import org.scalasteward.core.forge.gitea.GiteaApiAlg._ @@ -120,13 +120,13 @@ object GiteaApiAlg { } final class GiteaApiAlg[F[_]: HttpJsonClient]( - vcs: ForgeCfg, + forge: Gitea, modify: Request[F] => F[Request[F]] )(implicit logger: Logger[F], F: MonadThrow[F]) extends ForgeApiAlg[F] { def client: HttpJsonClient[F] = implicitly - val url = new Url(vcs.apiHost) + val url = new Url(forge.apiUri) val PULL_REQUEST_PAGE_SIZE: Int = 50 // default diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala index acdda79b8b..6a66ca789e 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala @@ -16,31 +16,33 @@ package org.scalasteward.core.forge.github -import cats.MonadThrow +import cats.effect.Concurrent import cats.syntax.all._ import io.circe.Json -import org.http4s.{Request, Uri} +import org.http4s.{Header, Request} import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.GitHub import org.scalasteward.core.forge.ForgeApiAlg import org.scalasteward.core.forge.data._ import org.scalasteward.core.forge.github.GitHubException._ import org.scalasteward.core.git.Branch import org.scalasteward.core.util.HttpJsonClient +import org.typelevel.ci.CIStringSyntax import org.typelevel.log4cats.Logger final class GitHubApiAlg[F[_]]( - gitHubApiHost: Uri, - modify: Request[F] => F[Request[F]] + forge: GitHub, + auth: Request[F] => F[Request[F]] )(implicit client: HttpJsonClient[F], logger: Logger[F], - F: MonadThrow[F] + F: Concurrent[F] ) extends ForgeApiAlg[F] { - private val url = new Url(gitHubApiHost) + private val url = new Url(forge.apiUri) /** https://docs.github.com/en/rest/repos/forks?apiVersion=2022-11-28#create-a-fork */ override def createFork(repo: Repo): F[RepoOut] = - client.post[RepoOut](url.forks(repo), modify).flatTap { repoOut => + client.post[RepoOut](url.forks(repo), auth).flatTap { repoOut => F.raiseWhen(repoOut.parent.exists(_.archived))(RepositoryArchived(repo)) } @@ -51,7 +53,7 @@ final class GitHubApiAlg[F[_]]( .postWithBody[PullRequestOut, CreatePullRequestPayload]( uri = url.pulls(repo), body = payload, - modify = modify + modify = auth ) .adaptErr(SecondaryRateLimitExceeded.fromThrowable) @@ -77,7 +79,7 @@ final class GitHubApiAlg[F[_]]( .patchWithBody[PullRequestOut, UpdatePullRequestPayload]( uri = url.pull(repo, number), body = payload, - modify = modify + modify = auth ) .adaptErr(SecondaryRateLimitExceeded.fromThrowable) @@ -91,24 +93,24 @@ final class GitHubApiAlg[F[_]]( /** https://docs.github.com/en/rest/repos/branches?apiVersion=2022-11-28#get-branch */ override def getBranch(repo: Repo, branch: Branch): F[BranchOut] = - client.get(url.branches(repo, branch), modify) + client.get(url.branches(repo, branch), auth) /** https://docs.github.com/en/rest/repos?apiVersion=2022-11-28#get */ override def getRepo(repo: Repo): F[RepoOut] = - client.get[RepoOut](url.repos(repo), modify).flatTap { repoOut => + client.get[RepoOut](url.repos(repo), auth).flatTap { repoOut => F.raiseWhen(repoOut.archived)(RepositoryArchived(repo)) } /** https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests */ override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] = - client.get(url.listPullRequests(repo, head, base), modify) + client.get(url.listPullRequests(repo, head, base), auth) /** https://docs.github.com/en/rest/pulls?apiVersion=2022-11-28#update-a-pull-request */ override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] = client.patchWithBody[PullRequestOut, UpdateState]( url.pull(repo, number), UpdateState(PullRequestState.Closed), - modify + auth ) /** https://docs.github.com/en/rest/issues?apiVersion=2022-11-28#create-an-issue-comment */ @@ -118,7 +120,7 @@ final class GitHubApiAlg[F[_]]( comment: String ): F[Comment] = client - .postWithBody(url.comments(repo, number), Comment(comment), modify) + .postWithBody(url.comments(repo, number), Comment(comment), auth) /** https://docs.github.com/en/rest/reference/issues?apiVersion=2022-11-28#add-labels-to-an-issue */ @@ -131,7 +133,7 @@ final class GitHubApiAlg[F[_]]( .postWithBody[io.circe.Json, GitHubLabels]( url.issueLabels(repo, number), GitHubLabels(labels), - modify + auth ) .adaptErr(SecondaryRateLimitExceeded.fromThrowable) .void @@ -145,7 +147,7 @@ final class GitHubApiAlg[F[_]]( .postWithBody[Json, GitHubAssignees]( url.assignees(repo, number), GitHubAssignees(assignees), - modify + auth ) .void .handleErrorWith { error => @@ -161,7 +163,7 @@ final class GitHubApiAlg[F[_]]( .postWithBody[Json, GitHubReviewers]( url.reviewers(repo, number), GitHubReviewers(reviewers), - modify + auth ) .void .handleErrorWith { error => @@ -169,5 +171,7 @@ final class GitHubApiAlg[F[_]]( s"cannot request review from '${reviewers.mkString(",")}' for PR '$number'" ) } - +} +object GitHubApiAlg { + val acceptHeaderVersioned: Header.Raw = Header.Raw(ci"Accept", "application/vnd.github.v3+json") } diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApp.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApp.scala deleted file mode 100644 index 8c05114876..0000000000 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApp.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.github - -import better.files.File - -case class GitHubApp(id: Long, keyFile: File) diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAppApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAppApiAlg.scala deleted file mode 100644 index 5c76814dce..0000000000 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAppApiAlg.scala +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.github - -import cats.effect.Concurrent -import cats.syntax.all._ -import org.http4s.{Header, Uri} -import org.scalasteward.core.util.HttpJsonClient -import org.typelevel.ci._ - -class GitHubAppApiAlg[F[_]]( - gitHubApiHost: Uri -)(implicit - client: HttpJsonClient[F], - F: Concurrent[F] -) { - - private[this] val acceptHeader = - Header.Raw(ci"Accept", "application/vnd.github.v3+json") - - private[this] def addHeaders(jwt: String): client.ModReq = - req => - F.point( - req.putHeaders( - Header.Raw(ci"Authorization", s"Bearer $jwt"), - acceptHeader - ) - ) - - /** [[https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-installations-for-the-authenticated-app]] - */ - def installations(jwt: String): F[List[InstallationOut]] = - client - .getAll[List[InstallationOut]]( - (gitHubApiHost / "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]] - */ - def accessToken(jwt: String, installationId: Long): F[TokenOut] = - client.post( - gitHubApiHost / "app" / "installations" / installationId.toString / "access_tokens", - addHeaders(jwt) - ) - - /** [[https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-repositories-accessible-to-the-app-installation]] - */ - def repositories(token: String): F[RepositoriesOut] = - client - .getAll[RepositoriesOut]( - (gitHubApiHost / "installation" / "repositories").withQueryParam("per_page", 100), - req => - F.point( - req.putHeaders( - Header.Raw(ci"Authorization", s"token $token"), - acceptHeader - ) - ) - ) - .compile - .toList - .map(values => - RepositoriesOut( - values.flatMap(_.repositories) - ) - ) -} 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..4b10253954 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,150 @@ 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.forge.github.GitHubApiAlg.acceptHeaderVersioned +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)), acceptHeaderVersioned) + 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 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"), + acceptHeaderVersioned + ) + ) + ) + .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 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 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)), + acceptHeaderVersioned + ) + ) } 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/GitLabApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala index 63da7a5427..508da32c48 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala @@ -23,8 +23,8 @@ import io.circe._ import io.circe.generic.semiauto._ import io.circe.syntax._ import org.http4s.{Request, Status, Uri} -import org.scalasteward.core.application.Config.{ForgeCfg, GitLabCfg} import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.GitLab import org.scalasteward.core.forge.ForgeApiAlg import org.scalasteward.core.forge.data._ import org.scalasteward.core.git.{Branch, Sha1} @@ -158,8 +158,7 @@ private[gitlab] object GitLabJsonCodec { } final class GitLabApiAlg[F[_]: Parallel]( - forgeCfg: ForgeCfg, - gitLabCfg: GitLabCfg, + forge: GitLab, modify: Request[F] => F[Request[F]] )(implicit client: HttpJsonClient[F], @@ -168,14 +167,14 @@ final class GitLabApiAlg[F[_]: Parallel]( ) extends ForgeApiAlg[F] { import GitLabJsonCodec._ - private val url = new Url(forgeCfg.apiHost) + private val url = new Url(forge.apiUri) override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] = client.get(url.listMergeRequests(repo, head, base.name), modify) override def createFork(repo: Repo): F[RepoOut] = { - val userOwnedRepo = repo.copy(owner = forgeCfg.login) - val data = ForkPayload(url.encodedProjectId(userOwnedRepo), forgeCfg.login) + val userOwnedRepo = repo.copy(owner = forge.login) + val data = ForkPayload(url.encodedProjectId(userOwnedRepo), forge.login) client .postWithBody[RepoOut, ForkPayload](url.createFork(repo), data, modify) .recoverWith { @@ -187,7 +186,7 @@ final class GitLabApiAlg[F[_]: Parallel]( } override def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut] = { - val targetRepo = if (forgeCfg.doNotFork) repo else repo.copy(owner = forgeCfg.login) + val targetRepo = if (forge.doNotFork) repo else repo.copy(owner = forge.login) val mergeRequest = for { projectId <- client.get[ProjectId](url.repos(repo), modify) usernameMapping <- getUsernameToUserIdsMapping((data.assignees ++ data.reviewers).toSet) @@ -196,7 +195,7 @@ final class GitLabApiAlg[F[_]: Parallel]( projectId = projectId.id, data = data, usernamesToUserIdsMapping = usernameMapping, - removeSourceBranch = gitLabCfg.removeSourceBranch + removeSourceBranch = forge.removeSourceBranch ) res <- client.postWithBody[MergeRequestOut, MergeRequestPayload]( uri = url.mergeRequest(targetRepo), @@ -233,7 +232,7 @@ final class GitLabApiAlg[F[_]: Parallel]( } val updatedMergeRequest = - if (!gitLabCfg.mergeWhenPipelineSucceeds) + if (!forge.mergeWhenPipelineSucceeds) mergeRequest else { for { @@ -278,7 +277,7 @@ final class GitLabApiAlg[F[_]: Parallel]( } private def maybeSetReviewers(repo: Repo, mrOut: MergeRequestOut): F[MergeRequestOut] = - gitLabCfg.requiredReviewers match { + forge.requiredReviewers match { case Some(requiredReviewers) => for { _ <- logger.info( 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..b68e923d0c 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,21 @@ 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.Forge.{ + AzureRepos, + Bitbucket, + BitbucketServer, + GitHub, + GitLab, + Gitea +} 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], @@ -52,13 +60,12 @@ final class FileGitAlg[F[_]](config: GitCfg)(implicit .as(true) .recover { case ex: ProcessFailedException if ex.exitValue === 1 => false } - override def clone(repo: File, url: Uri): F[Unit] = - for { - rootDir <- workspaceAlg.rootDir - _ <- git_("clone", "-c", "clone.defaultRemoteName=origin", url.toString, repo.pathAsString)( - rootDir - ) - } yield () + override def clone(repo: File, url: Uri): F[Unit] = for { + rootDir <- workspaceAlg.rootDir + _ <- git_("clone", "-c", "clone.defaultRemoteName=origin", url.toString, repo.pathAsString)( + rootDir + ) + } yield () override def cloneExists(repo: File): F[Boolean] = fileAlg.isDirectory(repo / ".git") @@ -156,7 +163,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.forge match { + case forge: AzureRepos => Some(forge.gitAskPass) + case forge: Bitbucket => Some(forge.gitAskPass) + case forge: BitbucketServer => Some(forge.gitAskPass) + case _: GitHub => None + case forge: GitLab => Some(forge.gitAskPass) + case forge: Gitea => Some(forge.gitAskPass) + }).map("GIT_ASKPASS" -> _.pathAsString).toList + processAlg .exec(gitCmd ++ args.toList, repo, extraEnv, slurpOptions) .recoverWith { @@ -175,7 +190,7 @@ 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" 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/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala index 2ef236245d..c7f17bb474 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala @@ -19,14 +19,13 @@ package org.scalasteward.core.nurture import cats.effect.Concurrent import cats.syntax.all._ import cats.{Applicative, Id} -import org.scalasteward.core.application.Config.ForgeCfg import org.scalasteward.core.coursier.CoursierAlg import org.scalasteward.core.data.ProcessResult.{Created, Ignored, Updated} import org.scalasteward.core.data._ import org.scalasteward.core.edit.{EditAlg, EditAttempt} import org.scalasteward.core.forge.data.NewPullRequestData.{filterLabels, labelsFor} import org.scalasteward.core.forge.data._ -import org.scalasteward.core.forge.{ForgeApiAlg, ForgeRepoAlg} +import org.scalasteward.core.forge.{Forge, ForgeApiAlg, ForgeRepoAlg} import org.scalasteward.core.git.{Branch, Commit, GitAlg} import org.scalasteward.core.repoconfig.PullRequestUpdateStrategy import org.scalasteward.core.util.logger.LoggerOps @@ -34,7 +33,7 @@ import org.scalasteward.core.util.{Nel, UrlChecker} import org.scalasteward.core.{git, util} import org.typelevel.log4cats.Logger -final class NurtureAlg[F[_]](config: ForgeCfg)(implicit +final class NurtureAlg[F[_]](forge: Forge)(implicit coursierAlg: CoursierAlg[F], editAlg: EditAlg[F], forgeApiAlg: ForgeApiAlg[F], @@ -61,7 +60,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit private def cloneAndSync(repo: Repo, fork: RepoOut): F[Branch] = for { _ <- gitAlg.cloneExists(repo).ifM(F.unit, forgeRepoAlg.cloneAndSync(repo, fork)) - baseBranch <- forgeApiAlg.parentOrRepo(fork, config.doNotFork).map(_.default_branch) + baseBranch <- forgeApiAlg.parentOrRepo(fork, forge.doNotFork).map(_.default_branch) } yield baseBranch private def updateDependencies( @@ -92,7 +91,7 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit private def processUpdate(data: UpdateData): F[ProcessResult] = for { _ <- logger.info(s"Process update ${data.update.show}") - head = config.tpe.pullRequestHeadFor(data.fork, data.updateBranch) + head = forge.pullRequestHeadFor(data.fork, data.updateBranch) pullRequests <- forgeApiAlg.listPullRequests(data.repo, head, data.baseBranch) result <- pullRequests.headOption match { case Some(pr) if pr.state.isClosed && data.update.isInstanceOf[Update.Single] => @@ -234,12 +233,12 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit labels = filterLabels(allLabels, data.repoData.config.pullRequests.includeMatchedLabels) } yield NewPullRequestData.from( data = data, - branchName = config.tpe.pullRequestHeadFor(data.fork, data.updateBranch), + branchName = forge.pullRequestHeadFor(data.fork, data.updateBranch), edits = edits, artifactIdToUrl = artifactIdToUrl, artifactIdToUpdateInfoUrls = artifactIdToUpdateInfoUrls.toMap, filesWithOldVersion = filesWithOldVersion, - addLabels = config.addLabels, + addLabels = forge.addLabels, labels = data.repoData.config.pullRequests.customLabels ++ labels ) diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinder.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinder.scala index bebf18a975..7f2f348423 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinder.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/UpdateInfoUrlFinder.scala @@ -19,17 +19,15 @@ package org.scalasteward.core.nurture import cats.Monad import cats.syntax.all._ import org.http4s.Uri -import org.scalasteward.core.application.Config.ForgeCfg import org.scalasteward.core.coursier.DependencyMetadata import org.scalasteward.core.data.Version -import org.scalasteward.core.forge.ForgeRepo -import org.scalasteward.core.forge.ForgeType._ +import org.scalasteward.core.forge.ForgeRepo.GitHub +import org.scalasteward.core.forge.{Forge, ForgeRepo} import org.scalasteward.core.nurture.UpdateInfoUrl._ import org.scalasteward.core.nurture.UpdateInfoUrlFinder.possibleUpdateInfoUrls import org.scalasteward.core.util.UrlChecker -final class UpdateInfoUrlFinder[F[_]](implicit - config: ForgeCfg, +final class UpdateInfoUrlFinder[F[_]](forge: Forge)(implicit urlChecker: UrlChecker[F], F: Monad[F] ) { @@ -39,9 +37,10 @@ final class UpdateInfoUrlFinder[F[_]](implicit ): F[List[UpdateInfoUrl]] = { val updateInfoUrls: List[UpdateInfoUrl] = dependency.releaseNotesUrl.toList.map(CustomReleaseNotes.apply) ++ - dependency.forgeRepo.toSeq.flatMap(forgeRepo => - possibleUpdateInfoUrls(forgeRepo, versionUpdate) - ) + dependency + .forgeRepo(forge) + .toSeq + .flatMap(forgeRepo => possibleUpdateInfoUrls(forgeRepo, versionUpdate)) updateInfoUrls .sorted(UpdateInfoUrl.updateInfoUrlOrder.toOrdering) @@ -98,12 +97,11 @@ object UpdateInfoUrlFinder { forgeRepo: ForgeRepo, version: Version ): List[UpdateInfoUrl] = - forgeRepo.forgeType match { - case GitHub => - Version.tagNames - .map(tagName => - GitHubReleaseNotes(forgeRepo.repoUrl / "releases" / "tag" / tagName(version)) - ) + forgeRepo match { + case GitHub(repoUrl) => + Version.tagNames.map(tagName => + GitHubReleaseNotes(repoUrl / "releases" / "tag" / tagName(version)) + ) case _ => Nil } diff --git a/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala index 38ec77a6f4..087eebd6df 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala @@ -43,7 +43,7 @@ final class RepoCacheAlg[F[_]](config: Config)(implicit logger.info(s"Check cache of ${repo.show}") >> refreshErrorAlg.skipIfFailedRecently(repo) { ( - forgeApiAlg.createForkOrGetRepoWithBranch(repo, config.forgeCfg.doNotFork), + forgeApiAlg.createForkOrGetRepoWithBranch(repo, config.forge.doNotFork), repoCacheRepository.findCache(repo) ).parTupled.flatMap { case ((repoOut, branchOut), maybeCache) => val latestSha1 = branchOut.commit.sha diff --git a/modules/core/src/main/scala/org/scalasteward/core/util/UrlChecker.scala b/modules/core/src/main/scala/org/scalasteward/core/util/UrlChecker.scala index d7437f2123..fb2d2366cf 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/util/UrlChecker.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/util/UrlChecker.scala @@ -22,6 +22,7 @@ import com.github.benmanes.caffeine.cache.Caffeine import org.http4s.client.Client import org.http4s.{Method, Request, Status, Uri} import org.scalasteward.core.application.Config +import org.scalasteward.core.forge.ForgeAuthAlg import org.typelevel.log4cats.Logger import scalacache.Entry import scalacache.caffeine.CaffeineCache @@ -33,35 +34,31 @@ trait UrlChecker[F[_]] { final case class UrlCheckerClient[F[_]](client: Client[F]) extends AnyVal object UrlChecker { - private def buildCache[F[_]](config: Config)(implicit - F: Sync[F] - ): F[CaffeineCache[F, String, Status]] = - F.delay { - val cache = Caffeine - .newBuilder() - .maximumSize(16384L) - .expireAfterWrite(config.cacheTtl.length, config.cacheTtl.unit) - .build[String, Entry[Status]]() - CaffeineCache(cache) - } + private def buildCache[F[_]: Sync](config: Config): CaffeineCache[F, String, Status] = { + val cache = Caffeine + .newBuilder() + .maximumSize(16384L) + .expireAfterWrite(config.cacheTtl.length, config.cacheTtl.unit) + .build[String, Entry[Status]]() + CaffeineCache(cache) + } - def create[F[_]](config: Config, modify: Request[F] => F[Request[F]])(implicit + def create[F[_]](config: Config)(implicit urlCheckerClient: UrlCheckerClient[F], + forgeAuthAlg: ForgeAuthAlg[F], logger: Logger[F], F: Sync[F] - ): F[UrlChecker[F]] = - buildCache(config).map { statusCache => - new UrlChecker[F] { - override def exists(url: Uri): F[Boolean] = - status(url).map(_ === Status.Ok).handleErrorWith { throwable => - logger.debug(throwable)(s"Failed to check if $url exists").as(false) - } + ): UrlChecker[F] = + new UrlChecker[F] { + override def exists(url: Uri): F[Boolean] = + status(url).map(_ === Status.Ok).handleErrorWith { throwable => + logger.error(throwable)(s"Failed to check if $url exists").as(false) + } - private def status(url: Uri): F[Status] = - statusCache.cachingF(url.renderString)(None) { - val req = Request[F](method = Method.HEAD, uri = url) - modify(req).flatMap(urlCheckerClient.client.status) - } - } + private def status(url: Uri): F[Status] = + buildCache(config).cachingF(url.renderString)(None) { + val req = Request[F](method = Method.HEAD, uri = url) + forgeAuthAlg.authenticateApi(req).flatMap(urlCheckerClient.client.status) + } } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala index b2e3ebf2e7..74f581dcb0 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala @@ -10,23 +10,19 @@ import org.scalasteward.core.application.ExitCodePolicy.{ SuccessIfAnyRepoSucceeds, SuccessOnlyIfAllReposSucceed } -import org.scalasteward.core.forge.ForgeType -import org.scalasteward.core.forge.github.GitHubApp +import org.scalasteward.core.forge.Forge.{AzureRepos, Bitbucket, GitHub, GitLab, Gitea} import org.scalasteward.core.util.Nel - import scala.concurrent.duration._ class CliTest extends FunSuite { - test("parseArgs: example") { + test("parseArgs: default GitHub") { val Success(Usage.Regular(obtained)) = Cli.parseArgs( List( + List("--github"), List("--workspace", "a"), List("--repos-file", "b"), List("--git-author-email", "d"), - List("--forge-type", "gitlab"), List("--forge-api-host", "http://example.com"), - List("--forge-login", "e"), - List("--git-ask-pass", "f"), List("--ignore-opts-files"), List("--env-var", "g=h"), List("--env-var", "i=j"), @@ -37,18 +33,13 @@ class CliTest extends FunSuite { List("--repo-config", "/opt/scala-steward/scala-steward.conf"), List("--github-app-id", "12345678"), List("--github-app-key-file", "example_app_key"), - List("--refresh-backoff-period", "1 day"), - List("--bitbucket-use-default-reviewers") + List("--refresh-backoff-period", "1 day") ).flatten ) assertEquals(obtained.workspace, File("a")) assertEquals(obtained.reposFiles, Nel.one(uri"b")) assertEquals(obtained.gitCfg.gitAuthor.email, "d") - assertEquals(obtained.gitCfg.gitAskPass, File("f")) - assertEquals(obtained.forgeCfg.tpe, ForgeType.GitLab) - assertEquals(obtained.forgeCfg.apiHost, uri"http://example.com") - assertEquals(obtained.forgeCfg.login, "e") assertEquals(obtained.ignoreOptsFiles, true) assertEquals(obtained.processCfg.envVars, List(EnvVar("g", "h"), EnvVar("i", "j"))) assertEquals(obtained.processCfg.processTimeout, 30.minutes) @@ -65,78 +56,56 @@ class CliTest extends FunSuite { obtained.artifactCfg.migrations, List(uri"/opt/scala-steward/extra-artifact-migrations.conf") ) - assertEquals(obtained.githubApp, Some(GitHubApp(12345678L, File("example_app_key")))) assertEquals(obtained.refreshBackoffPeriod, 1.day) - assert(!obtained.gitLabCfg.mergeWhenPipelineSucceeds) - assertEquals(obtained.gitLabCfg.requiredReviewers, None) - assert(obtained.bitbucketCfg.useDefaultReviewers) - assert(!obtained.bitbucketServerCfg.useDefaultReviewers) + obtained.forge match { + case forge: GitHub => + assertEquals(forge.apiUri, uri"http://example.com") + assertEquals(forge.appId, 12345678L) + assertEquals(forge.appKeyFile, File("example_app_key")) + case _ => fail(s"forge should be a ${classOf[GitHub].getName} instance") + } } - private val minimumRequiredParams = List( + private val minimumGithubRequiredParams = List( List("--workspace", "a"), List("--repos-file", "b"), List("--git-author-email", "d"), - List("--forge-login", "e"), - List("--git-ask-pass", "f"), - List("--disable-sandbox") - ) + List("--github-app-id", "12345678"), + List("--github-app-key-file", "example_app_key") + ).flatten - test("parseArgs: minimal example") { - val Success(Usage.Regular(obtained)) = Cli.parseArgs( - minimumRequiredParams.flatten - ) + test("parseArgs: minimal example for default GitHub") { + val Success(Usage.Regular(obtained)) = Cli.parseArgs(minimumGithubRequiredParams) assert(!obtained.processCfg.sandboxCfg.enableSandbox) assertEquals(obtained.workspace, File("a")) assertEquals(obtained.reposFiles, Nel.one(uri"b")) assertEquals(obtained.gitCfg.gitAuthor.email, "d") - assertEquals(obtained.gitCfg.gitAskPass, File("f")) - assertEquals(obtained.forgeCfg.login, "e") + obtained.forge match { + case forge: GitHub => + assertEquals(forge.appId, 12345678L) + assertEquals(forge.appKeyFile, File("example_app_key")) + case _ => fail(s"forge should be a ${classOf[GitHub].getName} instance") + } } test("parseArgs: enable sandbox") { - val Success(Usage.Regular(obtained)) = Cli.parseArgs( - List( - List("--workspace", "a"), - List("--repos-file", "b"), - List("--git-author-email", "d"), - List("--forge-login", "e"), - List("--git-ask-pass", "f"), - List("--enable-sandbox") - ).flatten - ) + val params = minimumGithubRequiredParams ++ List("--enable-sandbox") + val Success(Usage.Regular(obtained)) = Cli.parseArgs(params) assert(obtained.processCfg.sandboxCfg.enableSandbox) } test("parseArgs: sandbox parse error") { - val Error(obtained) = Cli.parseArgs( - List( - List("--workspace", "a"), - List("--repos-file", "b"), - List("--git-author-email", "d"), - List("--forge-login", "e"), - List("--git-ask-pass", "f"), - List("--enable-sandbox"), - List("--disable-sandbox") - ).flatten - ) + val params = minimumGithubRequiredParams ++ List("--enable-sandbox", "--disable-sandbox") + val Error(obtained) = Cli.parseArgs(params) assert(clue(obtained).startsWith("Unexpected option")) } test("parseArgs: disable sandbox") { - val Success(Usage.Regular(obtained)) = Cli.parseArgs( - List( - List("--workspace", "a"), - List("--repos-file", "b"), - List("--git-author-email", "d"), - List("--forge-login", "e"), - List("--git-ask-pass", "f"), - List("--disable-sandbox") - ).flatten - ) + val params = minimumGithubRequiredParams ++ List("--disable-sandbox") + val Success(Usage.Regular(obtained)) = Cli.parseArgs(params) assert(!obtained.processCfg.sandboxCfg.enableSandbox) } @@ -157,18 +126,34 @@ class CliTest extends FunSuite { } test("parseArgs: non-default GitLab arguments") { - val params = minimumRequiredParams ++ List( + val params = List( + List("--gitlab"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), List("--gitlab-merge-when-pipeline-succeeds"), List("--gitlab-required-reviewers", "5") ) val Success(Usage.Regular(obtained)) = Cli.parseArgs(params.flatten) - assert(obtained.gitLabCfg.mergeWhenPipelineSucceeds) - assertEquals(obtained.gitLabCfg.requiredReviewers, Some(5)) + obtained.forge match { + case forge: GitLab => + assert(forge.mergeWhenPipelineSucceeds) + assertEquals(forge.requiredReviewers, Some(5)) + case _ => fail(s"forge should be a ${classOf[GitLab].getName} instance") + } } test("parseArgs: invalid GitLab required reviewers") { - val params = minimumRequiredParams ++ List( + val params = List( + List("--gitlab"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), List("--gitlab-merge-when-pipeline-succeeds"), List("--gitlab-required-reviewers", "-3") ) @@ -178,64 +163,120 @@ class CliTest extends FunSuite { } test("parseArgs: validate-repo-config") { - val Success(Usage.ValidateRepoConfig(file)) = Cli.parseArgs( - List( - List("validate-repo-config", "file.conf") - ).flatten - ) + val params = List( + List("validate-repo-config", "file.conf") + ).flatten + val Success(Usage.ValidateRepoConfig(file)) = Cli.parseArgs(params) assertEquals(file, File("file.conf")) } test("parseArgs: validate fork mode disabled") { - val params = minimumRequiredParams ++ List( - List("--forge-type", "azure-repos"), - List("--do-not-fork") + val params = List( + List("--azure-repos"), + List("--forge-api-host", "a"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), + List("--azure-repos-organization=some-org") ) val Success(Usage.Regular(obtained)) = Cli.parseArgs(params.flatten) - assert(obtained.forgeCfg.doNotFork) + + obtained.forge match { + case forge: AzureRepos => + assertEquals(forge.doNotFork, true) + case _ => fail(s"forge should be a ${classOf[AzureRepos].getName} instance") + } } - test("parseArgs: validate fork mode enabled") { - val params = minimumRequiredParams ++ List( - List("--forge-type", "azure-repos") + test("parseArgs: validate no fork mode not allowed") { + val params = List( + List("--azure-repos"), + List("--forge-api-host", "a"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), + List("--do-not-fork"), + List("--azure-repos-organization=some-org") ) val Error(errorMsg) = Cli.parseArgs(params.flatten) - assert(clue(errorMsg).startsWith("azure-repos, bitbucket-server do not support fork mode")) + + assert(clue(errorMsg).startsWith("Unexpected option: --do-not-fork")) } - test("parseArgs: validate pull request labeling disabled") { - val params = minimumRequiredParams ++ List( - List("--forge-type", "bitbucket") + test("parseArgs: validate no fork mode enabled") { + val params = List( + List("--gitea"), + List("--forge-api-host", "a"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), + List("--do-not-fork") ) val Success(Usage.Regular(obtained)) = Cli.parseArgs(params.flatten) - assert(!obtained.forgeCfg.addLabels) + + obtained.forge match { + case forge: Gitea => + assertEquals(forge.doNotFork, true) + case _ => fail(s"forge should be a ${classOf[Gitea].getName} instance") + } } - test("parseArgs: exit code policy: --exit-code-success-if-any-repo-succeeds") { - val params = minimumRequiredParams ++ List( - List("--exit-code-success-if-any-repo-succeeds") + test("parseArgs: validate pull request labelling disabled") { + val params = List( + List("--bitbucket"), + List("--forge-api-host", "a"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), + List("--bitbucket-use-default-reviewers") ) val Success(Usage.Regular(obtained)) = Cli.parseArgs(params.flatten) + + obtained.forge match { + case forge: Bitbucket => + assertEquals(forge.addLabels, false) + case _ => fail(s"forge should be a ${classOf[Bitbucket].getName} instance") + } + } + + test("parseArgs: validate pull request labelling not allowed") { + val params = List( + List("--bitbucket"), + List("--forge-api-host", "a"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), + List("--bitbucket-use-default-reviewers"), + List("--add-labels") + ).flatten + val Error(errorMsg) = Cli.parseArgs(params) + + assert(clue(errorMsg).startsWith("Unexpected option: --add-labels")) + } + + test("parseArgs: exit code policy: --exit-code-success-if-any-repo-succeeds") { + val params = minimumGithubRequiredParams ++ List("--exit-code-success-if-any-repo-succeeds") + val Success(Usage.Regular(obtained)) = Cli.parseArgs(params) + assert(obtained.exitCodePolicy == SuccessIfAnyRepoSucceeds) } test("parseArgs: exit code policy: default") { - val Success(Usage.Regular(obtained)) = Cli.parseArgs(minimumRequiredParams.flatten) + val Success(Usage.Regular(obtained)) = Cli.parseArgs(minimumGithubRequiredParams) assert(obtained.exitCodePolicy == SuccessOnlyIfAllReposSucceed) } - test("parseArgs: validate pull request labeling enabled") { - val params = minimumRequiredParams ++ List( - List("--forge-type", "bitbucket"), - List("--add-labels") - ) - val Error(errorMsg) = Cli.parseArgs(params.flatten) - assert( - clue(errorMsg).startsWith("bitbucket, bitbucket-server do not support pull request labels") - ) - } - test("envVarArgument: env-var without equals sign") { assert(clue(Cli.envVarArgument.read("SBT_OPTS")).isInvalid) } @@ -245,17 +286,18 @@ class CliTest extends FunSuite { assertEquals(Cli.envVarArgument.read(s"SBT_OPTS=$value"), Valid(EnvVar("SBT_OPTS", value))) } - test("forgeTypeArgument: unknown value") { - assert(clue(Cli.forgeTypeArgument.read("sourceforge")).isInvalid) - } - test("azure-repos validation") { - val Error(error) = Cli.parseArgs( - (minimumRequiredParams ++ List( - List("--forge-type", "azure-repos"), - List("--azure-repos-organization") - )).flatten - ) + val param = List( + List("--azure-repos"), + List("--forge-api-host", "a"), + List("--workspace", "a"), + List("--repos-file", "b"), + List("--git-author-email", "d"), + List("--forge-login", "e"), + List("--git-ask-pass", "f"), + List("--azure-repos-organization") + ).flatten + val Error(error) = Cli.parseArgs(param) assert(error.startsWith("Missing value for option: --azure-repos-organization")) } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/application/RunResultsTest.scala b/modules/core/src/test/scala/org/scalasteward/core/application/RunResultsTest.scala index 99186661ec..e0b2744bce 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/application/RunResultsTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/application/RunResultsTest.scala @@ -20,6 +20,7 @@ import munit.FunSuite import org.scalasteward.core.data.Repo import scala.io.Source + class RunResultsTest extends FunSuite { private val repo1 = Repo("scala-steward-org", "scala-steward") private val repo2 = Repo("guardian", "play-secret-rotation") 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/buildtool/maven/MavenAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildtool/maven/MavenAlgTest.scala index 020bd417c7..86617cb2d4 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/buildtool/maven/MavenAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/buildtool/maven/MavenAlgTest.scala @@ -14,7 +14,7 @@ class MavenAlgTest extends FunSuite { val buildRoot = BuildRoot(repo, ".") val repoDir = workspaceAlg.repoDir(repo).unsafeRunSync() - val state = mavenAlg.getDependencies(buildRoot).runS(MockState.empty).unsafeRunSync() + val obtained = mavenAlg.getDependencies(buildRoot).runS(MockState.empty).unsafeRunSync() val expected = MockState.empty.copy( trace = Vector( Cmd.execSandboxed( @@ -33,6 +33,6 @@ class MavenAlgTest extends FunSuite { ) ) - assertEquals(state, expected) + assertEquals(obtained, expected) } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeAuthAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeAuthAlgTest.scala new file mode 100644 index 0000000000..52393379f6 --- /dev/null +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeAuthAlgTest.scala @@ -0,0 +1,79 @@ +package org.scalasteward.core.forge + +import better.files.File +import munit.CatsEffectSuite +import org.http4s.Credentials.Token +import org.http4s.headers.Authorization +import org.http4s.syntax.all._ +import org.http4s.{AuthScheme, BasicCredentials, Headers, Method, Request} +import org.scalasteward.core.forge.Forge.{GitHub, Gitea} +import org.scalasteward.core.forge.github.GitHubApiAlg.acceptHeaderVersioned +import org.scalasteward.core.forge.github.Repository +import org.scalasteward.core.io.ProcessAlg +import org.scalasteward.core.mock.MockConfig.{gitHubConfig, key} +import org.scalasteward.core.mock.MockContext.context.{httpJsonClient, logger, workspaceAlg} +import org.scalasteward.core.mock.{GitHubAuth, MockEff, MockState} + +class ForgeAuthAlgTest extends CatsEffectSuite { + test("authenticate Gitea API") { + val credentials = BasicCredentials("user", "pass") + implicit val processAlg: ProcessAlg[MockEff] = + new ProcessAlg(gitHubConfig.processCfg)(_ => MockEff.pure(List(credentials.password))) + val forge = Gitea( + apiUri = uri"https://git.example.com/api/v1", + login = credentials.username, + gitAskPass = File.newTemporaryFile(), + doNotFork = false, + addLabels = false + ) + val obtained = ForgeAuthAlg + .create[MockEff](forge) + .authenticateApi(Request[MockEff](method = Method.GET, uri = uri"")) + .runA(MockState.empty) + .map(_.headers) + val expected = Headers(Authorization(credentials)) + obtained.map(assertEquals(_, expected)) + } + + test("authenticate Gitea Git") { + val credentials = BasicCredentials("user", "pass") + implicit val processAlg: ProcessAlg[MockEff] = + new ProcessAlg(gitHubConfig.processCfg)(_ => MockEff.pure(List(credentials.password))) + val forge = Gitea( + apiUri = uri"https://git.example.com/api/v1", + login = credentials.username, + gitAskPass = File.newTemporaryFile(), + doNotFork = false, + addLabels = false + ) + val obtained = ForgeAuthAlg + .create[MockEff](forge) + .authenticateGit(uri"https://git.example.com/user/repo.git") + .runA(MockState.empty) + val expected = uri"https://user:pass@git.example.com/user/repo.git" + obtained.map(assertEquals(_, expected)) + } + + test("authenticate GitHub API") { + val state = MockState.empty.copy(clientResponses = GitHubAuth.api(List(Repository("user/bla")))) + implicit val processAlg: ProcessAlg[MockEff] = + new ProcessAlg(gitHubConfig.processCfg)(_ => MockEff.pure(List.empty)) + val forge = GitHub( + apiUri = uri"https://git.example.com", + doNotFork = false, + addLabels = false, + appId = 1L, + appKeyFile = key + ) + val obtained = ForgeAuthAlg + .create[MockEff](forge) + .authenticateApi( + Request[MockEff](method = Method.GET, uri = uri"https://git.example.com/repos/user/bla") + ) + .runA(state) + .map(_.headers) + val expected = + Headers(Authorization(Token(AuthScheme.Bearer, "some-token")), acceptHeaderVersioned) + obtained.map(assertEquals(_, expected)) + } +} 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..36588f63d4 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 @@ -3,12 +3,14 @@ package org.scalasteward.core.forge import munit.CatsEffectSuite import org.http4s.syntax.literals._ import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.GitHub 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.MockConfig.gitHubConfig +import org.scalasteward.core.mock.MockContext.context.{gitAlg, logger, workspaceAlg} import org.scalasteward.core.mock.MockState.TraceEntry.{Cmd, Log} -import org.scalasteward.core.mock.{MockConfig, MockEff, MockState} +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth +import org.scalasteward.core.mock.{MockEff, MockState} class ForgeRepoAlgTest extends CatsEffectSuite { private val repo = Repo("fthomas", "datapackage") @@ -29,16 +31,17 @@ 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](gitHubConfig).cloneAndSync(repo, forkRepoOut).runS(MockState.empty) val expected = MockState.empty.copy( trace = Vector( Log("Clone scala-steward/datapackage"), Cmd.git( - config.workspace, + gitHubConfig.workspace, "clone", "-c", "clone.defaultRemoteName=origin", @@ -58,13 +61,13 @@ class ForgeRepoAlgTest extends CatsEffectSuite { Cmd.git(repoDir, "submodule", "update", "--init", "--recursive") ) ) - state.map(assertEquals(_, expected)) + obtained.map(assertEquals(_, expected)) } test("cloneAndSync: doNotFork = true") { val config = - MockConfig.config.copy(forgeCfg = MockConfig.config.forgeCfg.copy(doNotFork = true)) - val state = new ForgeRepoAlg[MockEff](config) + gitHubConfig.copy(forge = gitHubConfig.forge.asInstanceOf[GitHub].copy(doNotFork = true)) + val obtained = new ForgeRepoAlg[MockEff](config) .cloneAndSync(repo, parentRepoOut) .runS(MockState.empty) @@ -84,11 +87,11 @@ class ForgeRepoAlgTest extends CatsEffectSuite { Cmd.git(repoDir, "submodule", "update", "--init", "--recursive") ) ) - state.map(assertEquals(_, expected)) + obtained.map(assertEquals(_, expected)) } test("cloneAndSync: doNotFork = false, no parent") { - forgeRepoAlg + new ForgeRepoAlg[MockEff](gitHubConfig) .cloneAndSync(repo, parentRepoOut) .runS(MockState.empty) .attempt diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoTest.scala index a29555541a..308f5b1cfc 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeRepoTest.scala @@ -3,7 +3,7 @@ package org.scalasteward.core.forge import munit.FunSuite import org.http4s.Uri import org.http4s.implicits._ -import org.scalasteward.core.forge.ForgeType._ +import org.scalasteward.core.forge.ForgeRepo._ /** As much as possible, uris in this test suite should aim to be real, clickable, uris that * actually go to real pages, allowing developers working against this test suite to verify that @@ -24,7 +24,7 @@ class ForgeRepoTest extends FunSuite { test("GitHub url patterns") { check( - ForgeRepo(GitHub, uri"https://github.com/scala-steward-org/scala-steward-action"), + GitHub(uri"https://github.com/scala-steward-org/scala-steward-action"), uri"https://github.com/scala-steward-org/scala-steward-action/blob/master/README.md", "v2.55.0" -> "v2.56.0", uri"https://github.com/scala-steward-org/scala-steward-action/compare/v2.55.0...v2.56.0" @@ -33,7 +33,7 @@ class ForgeRepoTest extends FunSuite { test("GitLab url patterns") { check( - ForgeRepo(GitLab, uri"https://gitlab.com/gitlab-org/gitlab"), + GitLab(uri"https://gitlab.com/gitlab-org/gitlab"), uri"https://gitlab.com/gitlab-org/gitlab/blob/master/README.md", "v15.11.8-ee" -> "v15.11.9-ee", uri"https://gitlab.com/gitlab-org/gitlab/compare/v15.11.8-ee...v15.11.9-ee" @@ -42,7 +42,7 @@ class ForgeRepoTest extends FunSuite { test("Gitea url patterns") { check( - ForgeRepo(Gitea, uri"https://gitea.com/lunny/levelqueue"), + Gitea(uri"https://gitea.com/lunny/levelqueue"), uri"https://gitea.com/lunny/levelqueue/src/branch/master/README.md", "v0.1.0" -> "v0.2.0", uri"https://gitea.com/lunny/levelqueue/compare/v0.1.0...v0.2.0" @@ -51,8 +51,7 @@ class ForgeRepoTest extends FunSuite { test("Azure url patterns") { check( - ForgeRepo( - AzureRepos, + AzureRepos( uri"https://dev.azure.com/rtyley/scala-steward-testing/_git/scala-steward-testing" ), uri"https://dev.azure.com/rtyley/scala-steward-testing/_git/scala-steward-testing?path=README.md", @@ -63,7 +62,7 @@ class ForgeRepoTest extends FunSuite { test("BitBucket url patterns") { check( - ForgeRepo(Bitbucket, uri"https://bitbucket.org/rtyley/scala-steward-test-repo"), + Bitbucket(uri"https://bitbucket.org/rtyley/scala-steward-test-repo"), uri"https://bitbucket.org/rtyley/scala-steward-test-repo/src/master/README.md", "v1.0.0" -> "v1.0.1", uri"https://bitbucket.org/rtyley/scala-steward-test-repo/compare/v1.0.1..v1.0.0#diff" @@ -72,7 +71,7 @@ class ForgeRepoTest extends FunSuite { test("BitBucket Server url patterns") { check( - ForgeRepo(BitbucketServer, uri"https://bitbucket-server.on-prem.com/foo/bar"), + BitbucketServer(uri"https://bitbucket-server.on-prem.com/foo/bar"), uri"https://bitbucket-server.on-prem.com/foo/bar/browse/README.md", "v1.0.0" -> "v1.0.1", uri"https://bitbucket-server.on-prem.com/foo/bar/compare/v1.0.1..v1.0.0#diff" 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/ForgeTypeTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeTypeTest.scala index 45e2cc9fb5..1c39421187 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeTypeTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/ForgeTypeTest.scala @@ -1,28 +1,31 @@ package org.scalasteward.core.forge +import better.files.File import munit.FunSuite +import org.http4s.Uri import org.scalasteward.core.TestSyntax._ import org.scalasteward.core.data.{Repo, Update} -import org.scalasteward.core.forge.ForgeType.{GitHub, GitLab} +import org.scalasteward.core.forge.Forge.{GitHub, GitLab} import org.scalasteward.core.git class ForgeTypeTest extends FunSuite { private val repo = Repo("foo", "bar") + private val dummyGitHub = GitHub(Uri.unsafeFromString(""), false, false, 0L, File("")) + private val dummyGitLab = + GitLab(Uri.unsafeFromString(""), "", File.newTemporaryFile(), false, false, false, None, false) // Single updates - { val update = ("ch.qos.logback".g % "logback-classic".a % "1.2.0" %> "1.2.3").single val updateBranch = git.branchFor(update, None) test("headFor (single)") { - assertEquals(GitHub.pullRequestHeadFor(repo, updateBranch), s"foo:${updateBranch.name}") - assertEquals(GitLab.pullRequestHeadFor(repo, updateBranch), updateBranch.name) + assertEquals(dummyGitHub.pullRequestHeadFor(repo, updateBranch), s"foo:${updateBranch.name}") + assertEquals(dummyGitLab.pullRequestHeadFor(repo, updateBranch), updateBranch.name) } } // Grouped updates - { val update = Update.Grouped( name = "my-group", @@ -33,13 +36,12 @@ class ForgeTypeTest extends FunSuite { val updateBranch = git.branchFor(update, None) test("headFor (grouped)") { - assertEquals(GitHub.pullRequestHeadFor(repo, updateBranch), s"foo:update/my-group") - assertEquals(GitLab.pullRequestHeadFor(repo, updateBranch), updateBranch.name) + assertEquals(dummyGitHub.pullRequestHeadFor(repo, updateBranch), s"foo:update/my-group") + assertEquals(dummyGitLab.pullRequestHeadFor(repo, updateBranch), updateBranch.name) } } // Grouped updates with hash - { val update = Update.Grouped( name = "my-group-${hash}", @@ -50,8 +52,11 @@ class ForgeTypeTest extends FunSuite { val updateBranch = git.branchFor(update, None) test("headFor (grouped) with $hash") { - assertEquals(GitHub.pullRequestHeadFor(repo, updateBranch), s"foo:update/my-group-1164623676") - assertEquals(GitLab.pullRequestHeadFor(repo, updateBranch), updateBranch.name) + assertEquals( + dummyGitHub.pullRequestHeadFor(repo, updateBranch), + s"foo:update/my-group-1164623676" + ) + assertEquals(dummyGitLab.pullRequestHeadFor(repo, updateBranch), updateBranch.name) } } 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..64f2a58c0a 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,26 +1,22 @@ package org.scalasteward.core.forge.azurerepos -import cats.syntax.semigroupk._ +import better.files.File 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 +import org.scalasteward.core.forge.Forge.AzureRepos import org.scalasteward.core.forge.data._ -import org.scalasteward.core.forge.{ForgeSelection, ForgeType} +import org.scalasteward.core.forge.ForgeApiAlg import org.scalasteward.core.git.{Branch, Sha1} -import org.scalasteward.core.mock.MockConfig.config +import org.scalasteward.core.mock.MockForgeAuthAlg.noAuth import org.scalasteward.core.mock.MockContext.context.httpJsonClient 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" object branchNameMatcher extends QueryParamDecoderMatcher[String]("name") object sourceRefNameMatcher @@ -28,16 +24,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 +42,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 +54,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 +80,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, @@ -172,11 +158,15 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> 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 state = MockState.empty.copy(clientResponses = httpApp) + private val forge = AzureRepos( + apiUri = uri"https://dev.azure.com", + login = "steward-user", + gitAskPass = File.newTemporaryFile(), + addLabels = true, + reposOrganization = "azure-org" + ) + private val azureReposApiAlg = ForgeApiAlg.create[MockEff](forge) 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..9137feaf04 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,34 +1,23 @@ package org.scalasteward.core.forge.bitbucket -import cats.syntax.semigroupk._ +import better.files.File 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 import org.scalasteward.core.data.Repo +import org.scalasteward.core.forge.Forge.Bitbucket import org.scalasteward.core.forge.data._ -import org.scalasteward.core.forge.{ForgeSelection, ForgeType} +import org.scalasteward.core.forge.ForgeApiAlg 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.MockForgeAuthAlg.noAuth import org.scalasteward.core.mock.{MockEff, MockState} 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,16 @@ 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 bitbucketCfg = BitbucketCfg(useDefaultReviewers = true) - private val bitbucketApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, bitbucketCfg, userM) + private val forge = Bitbucket( + apiUri = uri"https://api.bitbucket.org", + login = "some-user", + gitAskPass = File.newTemporaryFile(), + doNotFork = false, + useDefaultReviewers = true + ) + private val bitbucketApiAlg = ForgeApiAlg.create[MockEff](forge) 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..84e4773318 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,19 +1,18 @@ package org.scalasteward.core.forge.bitbucketserver -import cats.syntax.semigroupk._ +import better.files.File 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 +import org.scalasteward.core.forge.Forge.BitbucketServer import org.scalasteward.core.forge.data._ -import org.scalasteward.core.forge.{ForgeSelection, ForgeType} +import org.scalasteward.core.forge.ForgeApiAlg 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 BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { @@ -21,15 +20,7 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] 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) + private val forge = BitbucketServer( + apiUri = uri"http://example.org", + login = "some-user", + gitAskPass = File.newTemporaryFile(), + useDefaultReviewers = false + ) + private val bitbucketServerApiAlg = ForgeApiAlg.create[MockEff](forge) test("createPullRequest") { val data = NewPullRequestData( @@ -146,8 +141,13 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] assignees = Nil, reviewers = Nil ) - val apiAlg = ForgeSelection - .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = true), userM) + val forge = BitbucketServer( + apiUri = uri"http://example.org", + login = "some-user", + gitAskPass = File.newTemporaryFile(), + useDefaultReviewers = true + ) + val apiAlg = ForgeApiAlg.create[MockEff](forge) 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..3b8f7abf62 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,35 +1,25 @@ package org.scalasteward.core.forge.gitea -import cats.syntax.semigroupk._ +import better.files.File 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 +import org.scalasteward.core.forge.Forge.Gitea import org.scalasteward.core.forge.data._ -import org.scalasteward.core.forge.{ForgeSelection, ForgeType} +import org.scalasteward.core.forge.ForgeApiAlg 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 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,15 @@ class GiteaApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) - - private val forgeCfg = config.forgeCfg.copy( - tpe = ForgeType.Gitea, - apiHost = config.forgeCfg.apiHost / "api" / "v1" + private val state = MockState.empty.copy(clientResponses = httpApp) + private val forge = Gitea( + apiUri = uri"https://git.example.com/api/v1", + login = "some-user", + gitAskPass = File.newTemporaryFile(), + doNotFork = false, + addLabels = false ) - private val giteaAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GiteaCfg(), userM) + private val giteaAlg = ForgeApiAlg.create[MockEff](forge) 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..9d301a5890 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 @@ -1,5 +1,6 @@ package org.scalasteward.core.forge.github +import better.files.File import cats.effect.IO import cats.syntax.all._ import io.circe.literal._ @@ -7,30 +8,20 @@ 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 +import org.scalasteward.core.forge.Forge.GitHub import org.scalasteward.core.forge.data._ -import org.scalasteward.core.forge.{ForgeSelection, ForgeType} +import org.scalasteward.core.forge.ForgeApiAlg 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.MockContext.context.forgeAuthAlg import org.scalasteward.core.mock.{MockEff, MockState} +import org.scalasteward.core.mock.GitHubAuth 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 +187,17 @@ class GitHubApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) - - private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.GitHub) - private val gitHubApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GitHubCfg(), userM) + private val authApp = GitHubAuth.api(List(Repository("fthomas/cant-add-labels"))) + private val state = MockState.empty.copy(clientResponses = authApp <+> httpApp) + + private val forge = GitHub( + apiUri = uri"http://example.com", + doNotFork = false, + addLabels = false, + appId = 1L, + appKeyFile = File("/tmp/some.pem") + ) + private val gitHubApiAlg = ForgeApiAlg.create[MockEff](forge) 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 index 50574dec64..74936290e1 100644 --- 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 @@ -1,77 +1,77 @@ -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"))) - ) - } -} +//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..cc7e6c269f 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,26 +1,29 @@ package org.scalasteward.core.forge.github import better.files.File -import cats.effect.IO import munit.CatsEffectSuite - +import org.http4s.implicits.http4sLiteralsSyntax import scala.concurrent.duration._ +import org.scalasteward.core.mock.MockContext.context.httpJsonClient +import org.scalasteward.core.mock.MockContext.context.logger +import org.scalasteward.core.mock.{MockEff, MockState} class GitHubAuthAlgTest extends CatsEffectSuite { - 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 state = MockState.empty 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) 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..07c3199181 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 @@ -1,5 +1,6 @@ package org.scalasteward.core.forge.gitlab +import better.files.File import cats.syntax.semigroupk._ import io.circe.Json import io.circe.literal._ @@ -12,41 +13,25 @@ import org.http4s.headers.Allow import org.http4s.implicits._ import org.scalasteward.core.TestInstances.{dummyRepoCache, ioLogger} import org.scalasteward.core.TestSyntax._ -import org.scalasteward.core.application.Config.GitLabCfg import org.scalasteward.core.data.{Repo, RepoData, UpdateData} +import org.scalasteward.core.forge.Forge.GitLab import org.scalasteward.core.forge.data._ import org.scalasteward.core.forge.gitlab.GitLabJsonCodec._ -import org.scalasteward.core.forge.{ForgeSelection, ForgeType} +import org.scalasteward.core.forge.{ForgeApiAlg, ForgeAuthAlg} 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.MockContext.context.workspaceAlg +import org.scalasteward.core.mock.MockContext.processAlg 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") - object RequiredReviewersMatcher extends QueryParamDecoderMatcher[Int]("approvals_required") - 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" => Ok(getRepo) @@ -61,7 +46,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { } }""") - case POST -> Root / "projects" / s"${config.forgeCfg.login}/bar" / "merge_requests" => + case POST -> Root / "projects" / "user/bar" / "merge_requests" => Ok(getMr) case GET -> Root / "projects" / "foo/bar" / "merge_requests" => @@ -119,66 +104,30 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { case _ => NotFound() } - private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) - - private val gitlabApiAlg = ForgeSelection.forgeApiAlg[MockEff]( - config.forgeCfg.copy(tpe = ForgeType.GitLab), - GitLabCfg( - mergeWhenPipelineSucceeds = false, - requiredReviewers = None, - removeSourceBranch = false - ), - userM + private val state = MockState.empty.copy(clientResponses = httpApp) + private val forge = GitLab( + apiUri = uri"https://gitlab.com", + login = "user", + gitAskPass = File.newTemporaryFile(), + doNotFork = false, + addLabels = false, + mergeWhenPipelineSucceeds = false, + requiredReviewers = None, + removeSourceBranch = false ) - private val gitlabApiAlgNoFork = ForgeSelection.forgeApiAlg[MockEff]( - config.forgeCfg.copy(tpe = ForgeType.GitLab, doNotFork = true), - GitLabCfg( - mergeWhenPipelineSucceeds = false, - requiredReviewers = None, - removeSourceBranch = false - ), - userM + implicit private val forgeAuthAlg: ForgeAuthAlg[MockEff] = ForgeAuthAlg.create[MockEff](forge) + private val gitlabApiAlg = ForgeApiAlg.create[MockEff](forge) + private val gitlabApiAlgNoFork = ForgeApiAlg.create[MockEff](forge.copy(doNotFork = true)) + private val gitlabApiAlgAutoMerge = + ForgeApiAlg.create[MockEff](forge.copy(doNotFork = true, mergeWhenPipelineSucceeds = true)) + private val gitlabApiAlgRemoveSourceBranch = + ForgeApiAlg.create[MockEff](forge.copy(doNotFork = true, removeSourceBranch = true)) + private val gitlabApiAlgLessReviewersRequired = ForgeApiAlg.create[MockEff]( + forge.copy(doNotFork = true, mergeWhenPipelineSucceeds = true, requiredReviewers = Some(0)) ) - - private val gitlabApiAlgAutoMerge = ForgeSelection.forgeApiAlg[MockEff]( - config.forgeCfg.copy(tpe = ForgeType.GitLab, doNotFork = true), - GitLabCfg( - mergeWhenPipelineSucceeds = true, - requiredReviewers = None, - removeSourceBranch = false - ), - userM - ) - - private val gitlabApiAlgRemoveSourceBranch = ForgeSelection.forgeApiAlg[MockEff]( - config.forgeCfg.copy(tpe = ForgeType.GitLab, doNotFork = true), - GitLabCfg( - mergeWhenPipelineSucceeds = false, - requiredReviewers = None, - removeSourceBranch = true - ), - userM - ) - - private val gitlabApiAlgLessReviewersRequired = ForgeSelection.forgeApiAlg[MockEff]( - config.forgeCfg.copy(tpe = ForgeType.GitLab, doNotFork = true), - GitLabCfg( - mergeWhenPipelineSucceeds = true, - requiredReviewers = Some(0), - removeSourceBranch = false - ), - userM - ) - - private val gitlabApiAlgWithAssigneeAndReviewers = ForgeSelection.forgeApiAlg[MockEff]( - config.forgeCfg.copy(tpe = ForgeType.GitLab, doNotFork = true), - GitLabCfg( - mergeWhenPipelineSucceeds = true, - requiredReviewers = Some(0), - removeSourceBranch = false - ), - userM + private val gitlabApiAlgWithAssigneeAndReviewers = ForgeApiAlg.create[MockEff]( + forge.copy(doNotFork = true, mergeWhenPipelineSucceeds = true, requiredReviewers = Some(0)) ) private val data = UpdateData( @@ -291,7 +240,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 +279,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..da0d4c5da6 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 @@ -16,7 +16,7 @@ import org.scalasteward.core.git.FileGitAlgTest.{ import org.scalasteward.core.io.FileAlgTest.ioFileAlg import org.scalasteward.core.io.ProcessAlgTest.ioProcessAlg import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} -import org.scalasteward.core.mock.MockConfig.{config, mockRoot} +import org.scalasteward.core.mock.MockConfig.{gitHubConfig, mockRoot} import org.scalasteward.core.util.Nel class FileGitAlgTest extends CatsEffectSuite { @@ -221,7 +221,7 @@ object FileGitAlgTest { _ <- gitAlg.removeClone(repo) _ <- fileAlg.ensureExists(repo) _ <- git("-c", s"init.defaultBranch=${master.name}", "init", ".")(repo) - _ <- gitAlg.setAuthor(repo, config.gitCfg.gitAuthor) + _ <- gitAlg.setAuthor(repo, gitHubConfig.gitCfg.gitAuthor) _ <- git("commit", "--allow-empty", "-m", "Initial commit")(repo) } yield () @@ -283,10 +283,10 @@ object FileGitAlgTest { } implicit val ioWorkspaceAlg: WorkspaceAlg[IO] = - WorkspaceAlg.create[IO](config) + WorkspaceAlg.create[IO](gitHubConfig) implicit val ioGitAlg: GenGitAlg[IO, File] = - new FileGitAlg[IO](config.gitCfg).contramapRepoF(IO.pure) + new FileGitAlg[IO](gitHubConfig).contramapRepoF(IO.pure) val ioAuxGitAlg: AuxGitAlg[IO] = new AuxGitAlg[IO] diff --git a/modules/core/src/test/scala/org/scalasteward/core/io/MockWorkspaceAlg.scala b/modules/core/src/test/scala/org/scalasteward/core/io/MockWorkspaceAlg.scala index 1dbd52ea54..db13a1c11e 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/io/MockWorkspaceAlg.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/io/MockWorkspaceAlg.scala @@ -12,7 +12,7 @@ class MockWorkspaceAlg extends WorkspaceAlg[MockEff] { Kleisli.pure(()) override def rootDir: MockEff[File] = - Kleisli.pure(MockConfig.config.workspace) + Kleisli.pure(MockConfig.gitHubConfig.workspace) override def repoDir(repo: Repo): MockEff[File] = rootDir.map(_ / repo.owner / repo.repo) diff --git a/modules/core/src/test/scala/org/scalasteward/core/io/ProcessAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/io/ProcessAlgTest.scala index b98eaa0344..0fc3fc931d 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/io/ProcessAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/io/ProcessAlgTest.scala @@ -7,7 +7,7 @@ import munit.FunSuite import org.scalasteward.core.TestInstances._ import org.scalasteward.core.application.Config.{ProcessCfg, SandboxCfg} import org.scalasteward.core.io.ProcessAlgTest.ioProcessAlg -import org.scalasteward.core.mock.MockConfig.{config, mockRoot} +import org.scalasteward.core.mock.MockConfig.{gitHubConfig, mockRoot} import org.scalasteward.core.mock.MockState import org.scalasteward.core.mock.MockState.TraceEntry.Cmd import org.scalasteward.core.util.Nel @@ -33,7 +33,7 @@ class ProcessAlgTest extends FunSuite { test("execSandboxed: echo with enableSandbox = false") { val cfg = ProcessCfg(Nil, Duration.Zero, SandboxCfg(Nil, Nil, enableSandbox = false), 8192) - val state = MockProcessAlg + val obtained = MockProcessAlg .create(cfg) .execSandboxed(Nel.of("echo", "hello"), mockRoot) .runS(MockState.empty) @@ -43,7 +43,7 @@ class ProcessAlgTest extends FunSuite { trace = Vector(Cmd(mockRoot.toString, "echo", "hello")) ) - assertEquals(state, expected) + assertEquals(obtained, expected) } test("execSandboxed: echo with enableSandbox = true") { @@ -65,5 +65,5 @@ class ProcessAlgTest extends FunSuite { } object ProcessAlgTest { - implicit val ioProcessAlg: ProcessAlg[IO] = ProcessAlg.create[IO](config.processCfg) + implicit val ioProcessAlg: ProcessAlg[IO] = ProcessAlg.create[IO](gitHubConfig.processCfg) } 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..6bfd515be7 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,25 +4,33 @@ 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 - private val args: List[String] = List( - s"--workspace=$mockRoot/workspace", - s"--repos-file=$reposFile", - "--git-author-name=Bot Doe", - "--git-author-email=bot@example.org", - s"--git-ask-pass=$mockRoot/askpass.sh", - "--forge-api-host=http://example.com", - "--forge-login=bot-doe", - "--enable-sandbox", - "--env-var=VAR1=val1", - "--env-var=VAR2=val2", - "--cache-ttl=1hour", - "--add-labels", - "--refresh-backoff-period=1hour" - ) - val Success(Cli.Usage.Regular(config)) = Cli.parseArgs(args) + 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) + + val Success(Cli.Usage.Regular(gitHubConfig)) = { + val args: List[String] = List( + s"--workspace=$mockRoot/workspace", + s"--repos-file=$reposFile", + "--git-author-name=Bot Doe", + "--git-author-email=bot@example.org", + "--forge-api-host=https://github.com", + "--enable-sandbox", + "--env-var=VAR1=val1", + "--env-var=VAR2=val2", + "--cache-ttl=1hour", + "--add-labels", + "--refresh-backoff-period=1hour", + "--github-app-id=1234", + s"--github-app-key-file=$key" + ) + Cli.parseArgs(args) + } } diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala index 1bfcb8a466..c12394c46c 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala @@ -8,7 +8,7 @@ import org.scalasteward.core.application.{Config, Context, ValidateRepoConfigCon import org.scalasteward.core.edit.scalafix.ScalafixMigrationsLoader import org.scalasteward.core.io.FileAlgTest.ioFileAlg import org.scalasteward.core.io._ -import org.scalasteward.core.mock.MockConfig.config +import org.scalasteward.core.mock.MockConfig.gitHubConfig import org.scalasteward.core.repoconfig.RepoConfigLoader import org.scalasteward.core.update.artifact.ArtifactMigrationsLoader import org.scalasteward.core.util.UrlCheckerClient @@ -26,10 +26,10 @@ object MockContext { } } - implicit private val urlCheckerClient: UrlCheckerClient[MockEff] = UrlCheckerClient(client) + implicit val urlCheckerClient: UrlCheckerClient[MockEff] = UrlCheckerClient(client) implicit private val fileAlg: FileAlg[MockEff] = new MockFileAlg implicit private val logger: Logger[MockEff] = new MockLogger - implicit private val processAlg: ProcessAlg[MockEff] = MockProcessAlg.create(config.processCfg) + implicit val processAlg: ProcessAlg[MockEff] = MockProcessAlg.create(gitHubConfig.processCfg) implicit private val workspaceAlg: WorkspaceAlg[MockEff] = new MockWorkspaceAlg val mockState: MockState = MockState.empty.addUris( @@ -41,7 +41,7 @@ object MockContext { ioFileAlg.readResource("scalafix-migrations.conf").unsafeRunSync() ) - val context: Context[MockEff] = context(config) + val context: Context[MockEff] = context(gitHubConfig) def context(stewardConfig: Config): Context[MockEff] = mockState.toRef.flatMap(Context.step1(stewardConfig).run).unsafeRunSync() 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..f7256bc60f 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} @@ -84,8 +83,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) + val env = 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 c053214332..8eaa685322 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 @@ -7,10 +8,11 @@ import org.scalasteward.core.TestInstances._ import org.scalasteward.core.TestSyntax._ import org.scalasteward.core.data.{DependencyInfo, Repo, RepoData, UpdateData} import org.scalasteward.core.edit.EditAttempt.UpdateEdit +import org.scalasteward.core.forge.Forge.GitHub 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 +31,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() @@ -93,7 +95,9 @@ class NurtureAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { def nextToLast(L: Array[String]) = L(L.size - 2) val config = - MockConfig.config.copy(forgeCfg = MockConfig.config.forgeCfg.copy(addLabels = false)) + MockConfig.gitHubConfig.copy(forge = + MockConfig.gitHubConfig.forge.asInstanceOf[GitHub].copy(addLabels = false) + ) val nurtureAlg = context(config).nurtureAlg val repo = Repo("scala-steward-org", "scala-steward") val dependency = "org.typelevel".g % ("cats-effect", "cats-effect_2.13").a % "3.3.0" diff --git a/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala b/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala index 7633fec824..1648b729e0 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala @@ -9,13 +9,12 @@ import org.scalasteward.core.data.{Repo, Update} import org.scalasteward.core.forge.data.PullRequestState.Open import org.scalasteward.core.forge.data.{PullRequestNumber, PullRequestState} import org.scalasteward.core.git.{Branch, Sha1} -import org.scalasteward.core.mock.MockConfig.config +import org.scalasteward.core.mock.MockConfig.gitHubConfig import org.scalasteward.core.mock.MockContext.context.pullRequestRepository import org.scalasteward.core.mock.MockState.TraceEntry import org.scalasteward.core.mock.MockState.TraceEntry.Cmd import org.scalasteward.core.mock.{MockEff, MockState} import org.scalasteward.core.util.Nel - import java.util.concurrent.atomic.AtomicInteger class PullRequestRepositoryTest extends FunSuite { @@ -43,7 +42,7 @@ class PullRequestRepositoryTest extends FunSuite { val (state, output) = p(repo).runSA(MockState.empty).unsafeRunSync() val store = - config.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" + gitHubConfig.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" checkTrace( state, expectedStoreOps.map(op => Cmd(op, store.toString)).toVector 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..6b36943c2c 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 @@ -1,29 +1,32 @@ package org.scalasteward.core.nurture +import better.files.File import cats.syntax.all._ import munit.CatsEffectSuite import org.http4s.HttpApp import org.http4s.dsl.Http4sDsl import org.http4s.implicits._ -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.{ForgeRepo, ForgeType} +import org.scalasteward.core.forge.Forge.GitHub +import org.scalasteward.core.forge.ForgeRepo +import org.scalasteward.core.forge.github.Repository 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") @@ -81,17 +84,17 @@ class UpdateInfoUrlFinderTest extends CatsEffectSuite with Http4sDsl[MockEff] { assertIO(obtained, List.empty) } - implicit private val config: ForgeCfg = ForgeCfg( - ForgeType.GitHub, + private val forge = GitHub( uri"https://github.on-prem.com/", - "", doNotFork = false, - addLabels = false + addLabels = false, + appId = 1L, + appKeyFile = File("") ) - private val onPremUpdateUrlFinder = new UpdateInfoUrlFinder[MockEff] - private val gitHubFooBarRepo = ForgeRepo(GitHub, uri"https://github.com/foo/bar/") - private val bitbucketFooBarRepo = ForgeRepo(Bitbucket, uri"https://bitbucket.org/foo/bar/") - private val gitLabFooBarRepo = ForgeRepo(GitLab, uri"https://gitlab.com/foo/bar") + private val onPremUpdateUrlFinder = new UpdateInfoUrlFinder[MockEff](forge) + private val gitHubFooBarRepo = ForgeRepo.GitHub(uri"https://github.com/foo/bar/") + private val bitbucketFooBarRepo = ForgeRepo.Bitbucket(uri"https://bitbucket.org/foo/bar/") + private val gitLabFooBarRepo = ForgeRepo.GitLab(uri"https://gitlab.com/foo/bar") test("findUpdateInfoUrls: on-prem, repoUrl not found") { val metadata = @@ -161,7 +164,7 @@ class UpdateInfoUrlFinderTest extends CatsEffectSuite with Http4sDsl[MockEff] { ) assertEquals( - possibleVersionDiffs(ForgeRepo(GitHub, onPremForgeUrl.addPath("foo/bar")), versionUpdate) + possibleVersionDiffs(ForgeRepo.GitHub(onPremForgeUrl.addPath("foo/bar")), versionUpdate) .map(_.url.renderString), List( s"${onPremForgeUrl}foo/bar/compare/v$v1...v$v2", @@ -171,7 +174,7 @@ class UpdateInfoUrlFinderTest extends CatsEffectSuite with Http4sDsl[MockEff] { ) assertEquals( - possibleVersionDiffs(ForgeRepo(AzureRepos, onPremForgeUrl.addPath("foo/bar")), versionUpdate) + possibleVersionDiffs(ForgeRepo.AzureRepos(onPremForgeUrl.addPath("foo/bar")), versionUpdate) .map(_.url.renderString), List( s"${onPremForgeUrl}foo/bar/branchCompare?baseVersion=GTv$v1&targetVersion=GTv$v2", @@ -239,7 +242,7 @@ class UpdateInfoUrlFinderTest extends CatsEffectSuite with Http4sDsl[MockEff] { test("possibleUpdateInfoUrls: on-prem gitlab") { val obtained = possibleUpdateInfoUrls( - ForgeRepo(GitLab, uri"https://gitlab.on-prem.net/foo/bar"), + ForgeRepo.GitLab(uri"https://gitlab.on-prem.net/foo/bar"), versionUpdate ).map(_.url.renderString) val expected = possibleReleaseNotesFilenames.map(name => @@ -275,7 +278,7 @@ class UpdateInfoUrlFinderTest extends CatsEffectSuite with Http4sDsl[MockEff] { test("possibleUpdateInfoUrls: on-prem Bitbucket Server") { val repoUrl = uri"https://bitbucket-server.on-prem.com" / "foo" / "bar" val obtained = - possibleUpdateInfoUrls(ForgeRepo(BitbucketServer, repoUrl), versionUpdate).map(_.url) + possibleUpdateInfoUrls(ForgeRepo.BitbucketServer(repoUrl), versionUpdate).map(_.url) val expected = repoUrl / "browse" / "ReleaseNotes.md" assert(clue(obtained).contains(expected)) } diff --git a/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala b/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala index 31781840f5..bcada01cd5 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala @@ -3,7 +3,7 @@ package org.scalasteward.core.persistence import cats.effect.unsafe.implicits.global import cats.syntax.all._ import munit.FunSuite -import org.scalasteward.core.mock.MockConfig.config +import org.scalasteward.core.mock.MockConfig.gitHubConfig import org.scalasteward.core.mock.MockContext.context._ import org.scalasteward.core.mock.MockState.TraceEntry.Cmd import org.scalasteward.core.mock.{MockEff, MockState} @@ -20,9 +20,9 @@ class JsonKeyValueStoreTest extends FunSuite { val (state, value) = p.runSA(MockState.empty).unsafeRunSync() assertEquals(value, (Some("v1"), None)) - val k1File = config.workspace / "store" / "test-1" / "v0" / "k1" / "test-1.json" - val k2File = config.workspace / "store" / "test-1" / "v0" / "k2" / "test-1.json" - val k3File = config.workspace / "store" / "test-1" / "v0" / "k3" / "test-1.json" + val k1File = gitHubConfig.workspace / "store" / "test-1" / "v0" / "k1" / "test-1.json" + val k2File = gitHubConfig.workspace / "store" / "test-1" / "v0" / "k2" / "test-1.json" + val k3File = gitHubConfig.workspace / "store" / "test-1" / "v0" / "k3" / "test-1.json" val expected = MockState.empty.copy( trace = Vector( Cmd("write", k1File.toString), @@ -48,7 +48,7 @@ class JsonKeyValueStoreTest extends FunSuite { val (state, value) = p.runSA(MockState.empty).unsafeRunSync() assertEquals(value, Some("v0")) - val k1File = config.workspace / "store" / "test-2" / "v0" / "k1" / "test-2.json" + val k1File = gitHubConfig.workspace / "store" / "test-2" / "v0" / "k1" / "test-2.json" val expected = MockState.empty.copy( trace = Vector( Cmd("read", k1File.toString), @@ -72,8 +72,8 @@ class JsonKeyValueStoreTest extends FunSuite { val (state, value) = p.runSA(MockState.empty).unsafeRunSync() assertEquals(value, (Some("v1"), None)) - val k1File = config.workspace / "store" / "test-3" / "v0" / "k1" / "test-3.json" - val k2File = config.workspace / "store" / "test-3" / "v0" / "k2" / "test-3.json" + val k1File = gitHubConfig.workspace / "store" / "test-3" / "v0" / "k1" / "test-3.json" + val k2File = gitHubConfig.workspace / "store" / "test-3" / "v0" / "k2" / "test-3.json" val expected = MockState.empty.copy( trace = Vector(Cmd("write", k1File.toString), Cmd("read", k2File.toString)), files = Map(k1File -> """"v1"""") @@ -87,7 +87,7 @@ class JsonKeyValueStoreTest extends FunSuite { v1 <- kvStore.get("k1") } yield v1 - val k1File = config.workspace / "store" / "test-4" / "v0" / "k1" / "test-4.json" + val k1File = gitHubConfig.workspace / "store" / "test-4" / "v0" / "k1" / "test-4.json" val k1Mapping = k1File -> """ "v1 """ val value = MockState.empty.addFiles(k1Mapping).flatMap(p.runA).unsafeRunSync() assertEquals(value, None) 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 ) diff --git a/modules/core/src/test/scala/org/scalasteward/core/update/PruningAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/update/PruningAlgTest.scala index 5b5e2066db..ad2f72a1c5 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/update/PruningAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/update/PruningAlgTest.scala @@ -8,7 +8,7 @@ import org.scalasteward.core.TestInstances.dummyRepoCache import org.scalasteward.core.TestSyntax._ import org.scalasteward.core.data.Resolver.MavenRepository import org.scalasteward.core.data.{DependencyInfo, Repo, RepoData, Scope} -import org.scalasteward.core.mock.MockConfig.config +import org.scalasteward.core.mock.MockConfig.gitHubConfig import org.scalasteward.core.mock.MockContext.context.pruningAlg import org.scalasteward.core.mock.MockState import org.scalasteward.core.mock.MockState.TraceEntry.{Cmd, Log} @@ -30,7 +30,7 @@ class PruningAlgTest extends FunSuite { |}""".stripMargin ) val pullRequestsFile = - config.workspace / "store/pull_requests/v2/github/fthomas/scalafix-test/pull_requests.json" + gitHubConfig.workspace / "store/pull_requests/v2/github/fthomas/scalafix-test/pull_requests.json" val pullRequestsContent = s"""|{ | "https://github.com/fthomas/scalafix-test/pull/27" : { @@ -118,7 +118,7 @@ class PruningAlgTest extends FunSuite { |}""".stripMargin ) val pullRequestsFile = - config.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" + gitHubConfig.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" val pullRequestsContent = s"""|{ | "https://github.com/${repo.toPath}/pull/27" : { @@ -149,7 +149,7 @@ class PruningAlgTest extends FunSuite { | } |}""".stripMargin val versionsFile = - config.workspace / "store/versions/v2/https/foobar.org/maven2/org/scala-lang/scala-library/versions.json" + gitHubConfig.workspace / "store/versions/v2/https/foobar.org/maven2/org/scala-lang/scala-library/versions.json" val versionsContent = s"""|{ | "updatedAt" : 9999999999999, @@ -197,7 +197,7 @@ class PruningAlgTest extends FunSuite { ) val data = RepoData(repo, repoCache, RepoConfig.empty) val versionsFile = - config.workspace / "store/versions/v2/https/repo5.org/maven/org/scala-lang/scala-library/versions.json" + gitHubConfig.workspace / "store/versions/v2/https/repo5.org/maven/org/scala-lang/scala-library/versions.json" val versionsContent = s"""|{ | "updatedAt" : 9999999999999, @@ -263,7 +263,7 @@ class PruningAlgTest extends FunSuite { |}""".stripMargin ) val pullRequestsFile = - config.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" + gitHubConfig.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" val timestampNow = Instant.now().toEpochMilli val pullRequestsContent = s"""|{ @@ -295,7 +295,7 @@ class PruningAlgTest extends FunSuite { | } |}""".stripMargin val versionsFile = - config.workspace / "store/versions/v2/https/foobar.org/maven2/software/awssdk/s3/versions.json" + gitHubConfig.workspace / "store/versions/v2/https/foobar.org/maven2/software/awssdk/s3/versions.json" val versionsContent = s"""|{ | "updatedAt" : 9999999999999, @@ -375,7 +375,7 @@ class PruningAlgTest extends FunSuite { |}""".stripMargin ) val pullRequestsFile = - config.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" + gitHubConfig.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" val pullRequestsContent = s"""|{ | "https://github.com/${repo.toPath}/pull/27" : { @@ -406,7 +406,7 @@ class PruningAlgTest extends FunSuite { | } |}""".stripMargin val versionsFile = - config.workspace / "store/versions/v2/https/foobar.org/maven2/software/awssdk/s3/versions.json" + gitHubConfig.workspace / "store/versions/v2/https/foobar.org/maven2/software/awssdk/s3/versions.json" val versionsContent = s"""|{ | "updatedAt" : 9999999999999, @@ -491,7 +491,7 @@ class PruningAlgTest extends FunSuite { ) val timestampNow = Instant.now().toEpochMilli val pullRequestsFile = - config.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" + gitHubConfig.workspace / s"store/pull_requests/v2/github/${repo.toPath}/pull_requests.json" val pullRequestsContent = s"""|{ | "https://github.com/${repo.toPath}/pull/27" : { @@ -522,7 +522,7 @@ class PruningAlgTest extends FunSuite { | } |}""".stripMargin val versionsFile = - config.workspace / "store/versions/v2/https/foobar.org/maven2/software/awssdk/s3/versions.json" + gitHubConfig.workspace / "store/versions/v2/https/foobar.org/maven2/software/awssdk/s3/versions.json" val versionsContent = s"""|{ | "updatedAt" : 9999999999999,