diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..834f2d2 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1 @@ +version = 2.7.5 \ No newline at end of file diff --git a/README.md b/README.md index 83027e6..bebcedf 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ sources with `mvn package`. Next upload `target/slackIntegration.zip` to TeamCity `data/plugins/` folder (restart is needed). Create Slack App: -* Open [Create classic app](https://api.slack.com/apps?new_classic_app=1) form and fill it -* Go to **App Home**. Create Bot with **Add Legacy Bot User** button (fill both fields correctly) -* Go to **OAuth & Permissions**. Add scope **bot** in **Scopes** section. Then click **Install App in Workspace** -* Now copy **Bot User OAuth Access Token**. +* Open [Create app](https://api.slack.com/apps) form and fill it +* Go to **OAuth & Permissions**. Add following scopes in **Scopes** section: `channels:read`, `chat:write`, `chat:write.public`, `im:write`, `users:read`, `users:read.email`. If you plan to change sender name, add also `chat:write.customize` scope. +* Click **Install App in Workspace** +* Now copy **Bot User OAuth Token**. Paste this token into **Administration -> Slack -> OAuth Access Token** field. @@ -150,11 +150,6 @@ The message is prepended by Emoji ✅, ⛔ or ⚪ for successful, failed and oth ## Troubleshooting -**Q:** I followed all the instructions, but I get error message `not_allowed_token_type` when I try to save my Bot token into TeamCity! - -**A:** This plugin does not yet support the new Slack detailed OAuth scopes. When trying to save or use a token created for a Bot User using the new scopes, the Slack API will return that error message. When creating a new Slack App for this integration, do NOT opt into using the updated/beta scopes. If you have already opted in, you will need to create a new Slack App - there is no way to downgrade at this time. The only supported scope is the classic `bot` scope. - -![Beta Slack bot scopes](_doc/slack-beta-bot-scopes.png) **Q:** I checked the option to send private messages and added the `{mention}` placeholder to the message, but neither the message was send to the slack user nor the name was mentioned in the slack message! diff --git a/_doc/slack-beta-bot-scopes.png b/_doc/slack-beta-bot-scopes.png deleted file mode 100644 index a7742be..0000000 Binary files a/_doc/slack-beta-bot-scopes.png and /dev/null differ diff --git a/build/plugin-assembly.xml b/build/plugin-assembly.xml index 0b7b1ff..182c065 100644 --- a/build/plugin-assembly.xml +++ b/build/plugin-assembly.xml @@ -8,7 +8,7 @@ target/teamcity-plugin.xml - / + diff --git a/build/pom.xml b/build/pom.xml index 4252b12..a4463e7 100644 --- a/build/pom.xml +++ b/build/pom.xml @@ -10,7 +10,7 @@ pom yyyyMMddHHmmss - 1.8.1 + 2.0.0 Alex Kvak https://github.com/alexkvak diff --git a/pom.xml b/pom.xml index 880ea4e..9d8892f 100644 --- a/pom.xml +++ b/pom.xml @@ -27,17 +27,14 @@ org.apache.maven.plugins maven-compiler-plugin - 1.6 - 1.6 + 1.8 + 1.8 net.alchim31.maven scala-maven-plugin - 3.2.2 - - 2.12.1 - + 4.4.1 scala-compile-first @@ -67,7 +64,7 @@ org.scalatest scalatest-maven-plugin - 1.0 + 2.0.2 ${project.build.directory}/surefire-reports . diff --git a/slackIntegration-server/pom.xml b/slackIntegration-server/pom.xml index e546069..32dcd2e 100644 --- a/slackIntegration-server/pom.xml +++ b/slackIntegration-server/pom.xml @@ -9,6 +9,11 @@ slackIntegration-server jar + + + UTF-8 + + @@ -21,7 +26,7 @@ org.scala-lang scala-library - 2.12.1 + 2.13.5 @@ -34,22 +39,28 @@ org.scalatest - scalatest_2.12 - 3.0.3 + scalatest_2.13 + 3.2.6 test org.scalamock - scalamock-scalatest-support_2.12 - 3.6.0 + scalamock_2.13 + 5.1.0 test - com.ullink.slack - simpleslackapi - 1.0.0 + com.slack.api + bolt-socket-mode + 1.6.2 + + + org.slf4j + slf4j-api + + @@ -68,14 +79,14 @@ org.json4s - json4s-native_2.12 - 3.5.1 + json4s-native_2.13 + 3.6.11 org.json4s - json4s-ext_2.12 - 3.5.1 + json4s-ext_2.13 + 3.6.11 diff --git a/slackIntegration-server/src/main/resources/buildServerResources/configPage.jsp b/slackIntegration-server/src/main/resources/buildServerResources/configPage.jsp index b938690..b3a69b0 100644 --- a/slackIntegration-server/src/main/resources/buildServerResources/configPage.jsp +++ b/slackIntegration-server/src/main/resources/buildServerResources/configPage.jsp @@ -35,6 +35,10 @@ +
+ Send message as other user.
+ Please make sure that you granted the scope chat:write.customize +
diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/ConfigManager.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/ConfigManager.scala index 2b0c2d5..09a8136 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/ConfigManager.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/ConfigManager.scala @@ -19,9 +19,13 @@ class ConfigManager(paths: ServerPaths) { override def alwaysEscapeUnicode: Boolean = true } + new EnumNameSerializer(BuildSettingFlag) - private def configFile = new File(s"${paths.getConfigDir}/slackIntegration.json") + private def configFile = new File( + s"${paths.getConfigDir}/slackIntegration.json" + ) - private def backupFile = new File(s"${paths.getConfigDir}/slackIntegration.json.bak") + private def backupFile = new File( + s"${paths.getConfigDir}/slackIntegration.json.bak" + ) private[teamcity] var config: Option[Config] = readConfig @@ -31,18 +35,22 @@ class ConfigManager(paths: ServerPaths) { def oauthKey: Option[String] = config.map(_.oauthKey) def publicUrl: Option[String] = config.flatMap(_.publicUrl) - def senderName: Option[String] = config.flatMap(_.senderName).filter(_.nonEmpty) + def senderName: Option[String] = + config.flatMap(_.senderName).filter(_.nonEmpty) def enabled: Option[Boolean] = config.flatMap(_.enabled) def personalEnabled: Option[Boolean] = config.flatMap(_.personalEnabled) def sendAsAttachment: Option[Boolean] = config.flatMap(_.sendAsAttachment) - def allBuildSettingList: BuildSettings = config.map(_.buildSettings).getOrElse(Map.empty) + def allBuildSettingList: BuildSettings = + config.map(_.buildSettings).getOrElse(Map.empty) - def buildSettingList(buildTypeId: String): BuildSettings = allBuildSettingList.filter { - case (_, setting) ⇒ setting.buildTypeId == buildTypeId - } + def buildSettingList(buildTypeId: String): BuildSettings = + allBuildSettingList.filter { case (_, setting) => + setting.buildTypeId == buildTypeId + } - def buildSetting(id: String): Option[BuildSetting] = allBuildSettingList.get(id) + def buildSetting(id: String): Option[BuildSetting] = + allBuildSettingList.get(id) private def updateAndPersist(newConfig: Config): Boolean = { backup() @@ -58,54 +66,74 @@ class ConfigManager(paths: ServerPaths) { val out = new PrintWriter(file, "UTF-8") try { writePretty(config, out) - } - finally { + } finally { out.close() } true } - def updateBuildSetting(setting: BuildSetting, keyOption: Option[String]): Option[Boolean] = config.map { c ⇒ + def updateBuildSetting( + setting: BuildSetting, + keyOption: Option[String] + ): Option[Boolean] = config.map { c => val newSettings = keyOption match { - case Some(key) ⇒ + case Some(key) => c.buildSettings.updated(key, setting) - case _ ⇒ - c.buildSettings + (nextKey(allBuildSettingList) → setting) + case _ => + c.buildSettings + (nextKey(allBuildSettingList) -> setting) } updateAndPersist(c.copy(buildSettings = newSettings)) } - def update(authKey: String, pubUrl: String, personalEnabled: Boolean, enabled: Boolean, sender: String, sendAsAttachment: Boolean): Boolean = config match { - case Some(c) ⇒ - updateAndPersist(c.copy( - authKey, publicUrl = Some(pubUrl), personalEnabled = Some(personalEnabled), - enabled = Some(enabled), senderName = Some(sender), - sendAsAttachment = Some(sendAsAttachment) - )) - case None ⇒ - updateAndPersist(Config( - authKey, publicUrl = Some(pubUrl), personalEnabled = Some(personalEnabled), - enabled = Some(enabled), senderName = Some(sender), - sendAsAttachment = Some(sendAsAttachment) - )) + def update( + authKey: String, + pubUrl: String, + personalEnabled: Boolean, + enabled: Boolean, + sender: String, + sendAsAttachment: Boolean + ): Boolean = config match { + case Some(c) => + updateAndPersist( + c.copy( + authKey, + publicUrl = Some(pubUrl), + personalEnabled = Some(personalEnabled), + enabled = Some(enabled), + senderName = Some(sender), + sendAsAttachment = Some(sendAsAttachment) + ) + ) + case None => + updateAndPersist( + Config( + authKey, + publicUrl = Some(pubUrl), + personalEnabled = Some(personalEnabled), + enabled = Some(enabled), + senderName = Some(sender), + sendAsAttachment = Some(sendAsAttachment) + ) + ) } - def removeBuildSetting(key: String): Option[Boolean] = config.map { c ⇒ + def removeBuildSetting(key: String): Option[Boolean] = config.map { c => updateAndPersist(c.copy(buildSettings = c.buildSettings - key)) } def details: Map[String, Option[String]] = Map( - "oauthKey" → oauthKey, - "publicUrl" → publicUrl, - "senderName" → senderName, - "enabled" → enabled.filter(x ⇒ x).map(_ ⇒ "1"), - "personalEnabled" → personalEnabled.filter(x ⇒ x).map(_ ⇒ "1"), - "sendAsAttachment" → sendAsAttachment.filter(x ⇒ x).map(_ ⇒ "1") + "oauthKey" -> oauthKey, + "publicUrl" -> publicUrl, + "senderName" -> senderName, + "enabled" -> enabled.filter(x => x).map(_ => "1"), + "personalEnabled" -> personalEnabled.filter(x => x).map(_ => "1"), + "sendAsAttachment" -> sendAsAttachment.filter(x => x).map(_ => "1") ) - def isAvailable: Boolean = config.exists(c ⇒ c.enabled.exists(b ⇒ b) && c.oauthKey.length > 0) + def isAvailable: Boolean = + config.exists(c => c.enabled.exists(b => b) && c.oauthKey.nonEmpty) } object ConfigManager { @@ -113,23 +141,25 @@ object ConfigManager { object BuildSettingFlag extends Enumeration { type BuildSettingFlag = Value - val success, failureToSuccess, failure, successToFailure, canceled, started, queued = Value + val success, failureToSuccess, failure, successToFailure, canceled, started, + queued = Value } import BuildSettingFlag._ type BuildSettings = Map[String, BuildSetting] - case class BuildSetting(buildTypeId: String, - branchMask: String, - slackChannel: String, - messageTemplate: String, - flags: Set[BuildSettingFlag] = Set.empty, - artifactsMask: String = "", - deepLookup: Boolean = false, - notifyCommitter: Boolean = false, - maxVcsChanges: Int = BuildSetting.defaultMaxVCSChanges - ) { + case class BuildSetting( + buildTypeId: String, + branchMask: String, + slackChannel: String, + messageTemplate: String, + flags: Set[BuildSettingFlag] = Set.empty, + artifactsMask: String = "", + deepLookup: Boolean = false, + notifyCommitter: Boolean = false, + maxVcsChanges: Int = BuildSetting.defaultMaxVCSChanges + ) { // Getters for JSP lazy val getBranchMask: String = branchMask lazy val getSlackChannel: String = slackChannel @@ -140,15 +170,16 @@ object ConfigManager { lazy val getMaxVcsChanges: Int = maxVcsChanges // Flags lazy val getSuccess: Boolean = flags.contains(BuildSettingFlag.success) - lazy val getFailureToSuccess: Boolean = flags.contains(BuildSettingFlag.failureToSuccess) + lazy val getFailureToSuccess: Boolean = + flags.contains(BuildSettingFlag.failureToSuccess) lazy val getFail: Boolean = flags.contains(BuildSettingFlag.failure) - lazy val getSuccessToFailure: Boolean = flags.contains(BuildSettingFlag.successToFailure) + lazy val getSuccessToFailure: Boolean = + flags.contains(BuildSettingFlag.successToFailure) lazy val getCanceled: Boolean = flags.contains(BuildSettingFlag.canceled) lazy val getStarted: Boolean = flags.contains(BuildSettingFlag.started) lazy val getQueued: Boolean = flags.contains(BuildSettingFlag.queued) - /** - * Removes success flag if failureToSuccess is set + /** Removes success flag if failureToSuccess is set * and failure flag if successToFailure is set * * @return @@ -169,14 +200,15 @@ object ConfigManager { lazy val defaultMaxVCSChanges = 5 } - case class Config(oauthKey: String, - buildSettings: BuildSettings = Map.empty, - publicUrl: Option[String] = None, - personalEnabled: Option[Boolean] = Some(true), - sendAsAttachment: Option[Boolean] = Some(true), - enabled: Option[Boolean] = Some(true), - senderName: Option[String] = None - ) + case class Config( + oauthKey: String, + buildSettings: BuildSettings = Map.empty, + publicUrl: Option[String] = None, + personalEnabled: Option[Boolean] = Some(true), + sendAsAttachment: Option[Boolean] = Some(true), + enabled: Option[Boolean] = Some(true), + senderName: Option[String] = None + ) @annotation.tailrec private def nextKey(map: BuildSettings): String = { diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Helpers.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Helpers.scala index 22bb066..dfd1c1b 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Helpers.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Helpers.scala @@ -1,13 +1,17 @@ package com.fpd.teamcity.slack -import javax.servlet.http.HttpServletRequest - import jetbrains.buildServer.messages.Status -import jetbrains.buildServer.serverSide.{SBuild, SBuildServer, SFinishedBuild, SQueuedBuild} +import jetbrains.buildServer.serverSide.{ + SBuild, + SBuildServer, + SFinishedBuild, + SQueuedBuild +} import jetbrains.buildServer.users.SUser import jetbrains.buildServer.web.util.SessionUser -import scala.collection.JavaConverters._ +import javax.servlet.http.HttpServletRequest +import scala.jdk.CollectionConverters._ import scala.language.implicitConversions import scala.util.Random @@ -16,10 +20,13 @@ object Helpers { object Implicits { implicit class RichHttpServletRequest(val request: Request) extends AnyVal { - def param(key: String): Option[String] = Option(request.getParameter(key)).map(_.trim).filterNot(_.isEmpty) + def param(key: String): Option[String] = + Option(request.getParameter(key)).map(_.trim).filterNot(_.isEmpty) } - implicit def requestToUser(request: Request): Option[SUser] = Option(SessionUser.getUser(request)) + implicit def requestToUser(request: Request): Option[SUser] = Option( + SessionUser.getUser(request) + ) implicit class RichRandom(val random: Random) extends AnyVal { def randomAlphaNumericString(length: Int): String = { @@ -27,9 +34,12 @@ object Helpers { randomStringFromCharList(length, chars) } - private def randomStringFromCharList(length: Int, chars: Seq[Char]): String = { + private def randomStringFromCharList( + length: Int, + chars: Seq[Char] + ): String = { val sb = new StringBuilder - for (_ ← 1 to length) { + for (_ <- 1 to length) { val randomNum = util.Random.nextInt(chars.length) sb.append(chars(randomNum)) } @@ -43,13 +53,21 @@ object Helpers { implicit class RichBuild(val build: SBuild) extends AnyVal { def committees: Vector[SUser] = - build.getContainingChanges.asScala.toVector.flatMap(_.getCommitters.asScala).distinct + build.getContainingChanges.asScala.toVector + .flatMap(_.getCommitters.asScala) + .distinct def committeeEmails: Vector[String] = - committees.map(user ⇒ Option(user.getEmail)).collect { case Some(x) if x.length > 0 ⇒ x } + committees.map(user => Option(user.getEmail)).collect { + case Some(x) if x.nonEmpty => x + } def matchBranch(mask: String): Boolean = - mask.r.findFirstIn(Option(build.getBranch).map(_.getDisplayName).getOrElse("")).isDefined + mask.r + .findFirstIn( + Option(build.getBranch).map(_.getDisplayName).getOrElse("") + ) + .isDefined def formattedDuration: String = encodeDuration(build.getDuration) @@ -57,22 +75,34 @@ object Helpers { implicit class RichQueuedBuild(val build: SQueuedBuild) extends AnyVal { def matchBranch(mask: String): Boolean = - mask.r.findFirstIn(Option(build.getBuildPromotion.getBranch).map(_.getDisplayName).getOrElse("")).isDefined + mask.r + .findFirstIn( + Option(build.getBuildPromotion.getBranch) + .map(_.getDisplayName) + .getOrElse("") + ) + .isDefined } - implicit class RichBuildServer(val sBuildServer: SBuildServer) extends AnyVal { + implicit class RichBuildServer(val sBuildServer: SBuildServer) + extends AnyVal { def findPreviousStatus(build: SBuild): Status = { - def filterEntry(x: SFinishedBuild): Boolean = if (build.getBranch == null) + def filterEntry(x: SFinishedBuild): Boolean = if ( + build.getBranch == null + ) x.getBranch == null else - Option(x.getBranch).exists(_.getDisplayName == build.getBranch.getDisplayName) + Option(x.getBranch).exists( + _.getDisplayName == build.getBranch.getDisplayName + ) - val history = sBuildServer.getHistory.getEntriesBefore(build, false).asScala + val history = + sBuildServer.getHistory.getEntriesBefore(build, false).asScala history.view - .filter(filterEntry) // branch name filter - .filter(!_.isPersonal) // ignore personal builds + .filter(filterEntry) // branch name filter + .filter(!_.isPersonal) // ignore personal builds .map(_.getBuildStatus) .find(_ != Status.UNKNOWN) // ignore cancelled and aborted builds .getOrElse(Status.NORMAL) @@ -91,11 +121,20 @@ object Helpers { case seconds if seconds < oneMinute => result ::: List(s"${seconds}s") case seconds if seconds >= oneMinute && seconds < oneHour => - List(s"${seconds / oneMinute}m") ::: encodeDuration(result, seconds % oneMinute) + List(s"${seconds / oneMinute}m") ::: encodeDuration( + result, + seconds % oneMinute + ) case seconds if seconds >= oneHour && seconds < oneDay => - List(s"${seconds / oneHour}h") ::: encodeDuration(result, seconds % oneHour) + List(s"${seconds / oneHour}h") ::: encodeDuration( + result, + seconds % oneHour + ) case seconds => - List(s"${seconds / oneDay}d") ::: encodeDuration(result, seconds % oneDay) + List(s"${seconds / oneDay}d") ::: encodeDuration( + result, + seconds % oneDay + ) } } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Logger.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Logger.scala index 74f218a..3a50d6e 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Logger.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Logger.scala @@ -3,5 +3,6 @@ package com.fpd.teamcity.slack import jetbrains.buildServer.log.Loggers class Logger { - def log(message: String): Unit = Loggers.SERVER.info(s"${Strings.logCategory} - $message") + def log(message: String): Unit = + Loggers.SERVER.info(s"${Strings.logCategory} - $message") } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/MessageBuilder.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/MessageBuilder.scala index 868b8b3..ab3c24f 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/MessageBuilder.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/MessageBuilder.scala @@ -6,124 +6,195 @@ import com.fpd.teamcity.slack.SlackGateway.SlackAttachment import com.fpd.teamcity.slack.Strings.MessageBuilder._ import jetbrains.buildServer.messages.Status import jetbrains.buildServer.serverSide.artifacts.BuildArtifacts.BuildArtifactsProcessor.Continuation -import jetbrains.buildServer.serverSide.artifacts.{BuildArtifact, BuildArtifactsViewMode} -import jetbrains.buildServer.serverSide.{SBuild, SQueuedBuild, ServerPaths, WebLinks} +import jetbrains.buildServer.serverSide.artifacts.{ + BuildArtifact, + BuildArtifactsViewMode +} +import jetbrains.buildServer.serverSide.{ + SBuild, + SQueuedBuild, + ServerPaths, + WebLinks +} -import scala.collection.JavaConverters._ import scala.collection.mutable.ArrayBuffer +import scala.jdk.CollectionConverters._ abstract class MessageBuilder { def compile(template: String, setting: BuildSetting): SlackAttachment protected def encodeText(text: String): String = - text.replaceAllLiterally("&", "&") - .replaceAllLiterally("<", "<") - .replaceAllLiterally(">", ">") + text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") } -class SBuildMessageBuilder(build: SBuild, context: MessageBuilderContext) extends MessageBuilder { +class SBuildMessageBuilder(build: SBuild, context: MessageBuilderContext) + extends MessageBuilder { import Helpers.Implicits._ - override def compile(template: String, setting: BuildSetting): SlackAttachment = { + override def compile( + template: String, + setting: BuildSetting + ): SlackAttachment = { def status = if (build.getDuration == 0) { if (build.getBuildStatus.isSuccessful) statusStarted else statusCanceled - } - else if (build.getBuildStatus.isSuccessful) statusSucceeded + } else if (build.getBuildStatus.isSuccessful) statusSucceeded else statusFailed - def artifacts = s"<${context.getDownloadAllArtifactsUrl(build)}|Download all artifacts>" + def artifacts = + s"<${context.getDownloadAllArtifactsUrl(build)}|Download all artifacts>" - lazy val artifactsRelUrl = build.getArtifactsDirectory.getPath.stripPrefix(context.getArtifactsPath).stripPrefix("/") + lazy val artifactsRelUrl = build.getArtifactsDirectory.getPath + .stripPrefix(context.getArtifactsPath) + .stripPrefix("/") - def artifactLinks = if (!setting.artifactsMask.isEmpty) { + def artifactLinks = if (setting.artifactsMask.nonEmpty) { val links = ArrayBuffer.empty[String] val compiledMask = setting.artifactsMask.r - val publicUrl = context.artifactsPublicUrl.getOrElse("").reverse.dropWhile(_ == '/').reverse - - build.getArtifacts(BuildArtifactsViewMode.VIEW_DEFAULT_WITH_ARCHIVES_CONTENT).iterateArtifacts((artifact: BuildArtifact) ⇒ { - if (artifact.isFile && compiledMask.findFirstIn(artifact.getName).isDefined) { - links += s"$publicUrl/$artifactsRelUrl/${artifact.getRelativePath}" - } - - if (!artifact.isArchive && (artifact.getRelativePath == "" || !artifact.isDirectory || (artifact.isDirectory && setting.deepLookup))) - Continuation.CONTINUE - else - Continuation.SKIP_CHILDREN - }) + val publicUrl = context.artifactsPublicUrl + .getOrElse("") + .reverse + .dropWhile(_ == '/') + .reverse + + build + .getArtifacts(BuildArtifactsViewMode.VIEW_DEFAULT_WITH_ARCHIVES_CONTENT) + .iterateArtifacts((artifact: BuildArtifact) => { + if ( + artifact.isFile && compiledMask + .findFirstIn(artifact.getName) + .isDefined + ) { + links += s"$publicUrl/$artifactsRelUrl/${artifact.getRelativePath}" + } + + if ( + !artifact.isArchive && (artifact.getRelativePath == "" || !artifact.isDirectory || (artifact.isDirectory && setting.deepLookup)) + ) + Continuation.CONTINUE + else + Continuation.SKIP_CHILDREN + }) links.mkString("\n") } else "" - def changes = build.getContainingChanges.asScala.take(setting.maxVcsChanges).map { change ⇒ - val name = change.getCommitters.asScala.headOption.map(_.getDescriptiveName).getOrElse(change.getUserName) - s"- ${change.getDescription.takeWhile(_ != '\n')} [$name]" - } mkString "\n" - - def mentions = if (build.getBuildStatus.isSuccessful) "" else { - build.committeeEmails.map(context.userByEmail).collect { case Some(x) ⇒ s"<@$x>" }.mkString(" ") + def changes = + build.getContainingChanges.asScala.take(setting.maxVcsChanges).map { + change => + val name = change.getCommitters.asScala.headOption + .map(_.getDescriptiveName) + .getOrElse(change.getUserName) + s"- ${change.getDescription.takeWhile(_ != '\n')} [$name]" + } mkString "\n" + + def mentions = if (build.getBuildStatus.isSuccessful) "" + else { + build.committeeEmails + .map(context.userByEmail) + .collect { case Some(x) => s"<@$x>" } + .mkString(" ") } - def users = if (build.getBuildStatus.isSuccessful) "" else { - build.committees.map(user ⇒ user.getDescriptiveName).mkString(", ") + def users = if (build.getBuildStatus.isSuccessful) "" + else { + build.committees.map(user => user.getDescriptiveName).mkString(", ") } - def reason = if (build.getBuildStatus.isSuccessful) "" else { - "Reason: " + (if (build.getFailureReasons.isEmpty) unknownReason else build.getFailureReasons.asScala.map(_.getDescription).mkString("\n")) + def reason = if (build.getBuildStatus.isSuccessful) "" + else { + "Reason: " + (if (build.getFailureReasons.isEmpty) unknownReason + else + build.getFailureReasons.asScala + .map(_.getDescription) + .mkString("\n")) } - val text = """\{([\s\w-._%]+)}""".r.replaceAllIn(template, m ⇒ m.group(1) match { - case "name" ⇒ encodeText(build.getFullName) - case "number" ⇒ build.getBuildNumber - case "branch" ⇒ Option(build.getBranch).map(_.getDisplayName).getOrElse(unknownBranch) - case "status" ⇒ status - case "changes" ⇒ encodeText(changes) - case "allArtifactsDownloadUrl" ⇒ artifacts - case "artifactsRelUrl" ⇒ artifactsRelUrl - case "artifactLinks" ⇒ artifactLinks - case "link" ⇒ context.getViewResultsUrl(build) - case "mentions" ⇒ mentions - case "users" ⇒ users - case "formattedDuration" ⇒ build.formattedDuration - case "reason" ⇒ encodeText(reason) - case x if x.startsWith("%") && x.endsWith("%") ⇒ - context.getBuildParameter(build, x.substring(1, x.length - 1).trim) match { - case Some(value) ⇒ encodeText(value) - case _ ⇒ unknownParameter - } - case _ ⇒ m.group(0) - }) - - SlackAttachment(text.trim, statusColor(build.getBuildStatus), statusEmoji(build.getBuildStatus)) + val text = """\{([\s\w-._%]+)}""".r.replaceAllIn( + template, + m => + m.group(1) match { + case "name" => encodeText(build.getFullName) + case "number" => build.getBuildNumber + case "branch" => + Option(build.getBranch) + .map(_.getDisplayName) + .getOrElse(unknownBranch) + case "status" => status + case "changes" => encodeText(changes) + case "allArtifactsDownloadUrl" => artifacts + case "artifactsRelUrl" => artifactsRelUrl + case "artifactLinks" => artifactLinks + case "link" => context.getViewResultsUrl(build) + case "mentions" => mentions + case "users" => users + case "formattedDuration" => build.formattedDuration + case "reason" => encodeText(reason) + case x if x.startsWith("%") && x.endsWith("%") => + context.getBuildParameter( + build, + x.substring(1, x.length - 1).trim + ) match { + case Some(value) => encodeText(value) + case _ => unknownParameter + } + case _ => m.group(0) + } + ) + + SlackAttachment( + text.trim, + statusColor(build.getBuildStatus), + statusEmoji(build.getBuildStatus) + ) } } -class SQueuedBuildMessageBuilder(build: SQueuedBuild, context: MessageBuilderContext) extends MessageBuilder { - override def compile(template: String, setting: BuildSetting): SlackAttachment = { - - val text = """\{([\s\w-._%]+)}""".r.replaceAllIn(template, m ⇒ m.group(1) match { - case "name" ⇒ encodeText(context.getQueuedBuildName(build)) // "" // encodeText(build.getFullName) - case "number" ⇒ "" // has no number yet - case "branch" ⇒ context.getQueuedBuildBranch(build) - case "status" ⇒ statusQueued - case "changes" ⇒ "" // changes is unknown now - case "allArtifactsDownloadUrl" ⇒ "" - case "artifactsRelUrl" ⇒ "" - case "artifactLinks" ⇒ "" - case "link" ⇒ context.getQueuedBuildUrl(build) - case "mentions" ⇒ "" // changes is unknown now - case "users" ⇒ "" // changes is unknown now - case "formattedDuration" ⇒ "" // build only queued - case "reason" ⇒ "" - case x if x.startsWith("%") && x.endsWith("%") ⇒ - context.getQueuedBuildParameter(build, x.substring(1, x.length - 1).trim) match { - case Some(value) ⇒ encodeText(value) - case _ ⇒ unknownParameter +class SQueuedBuildMessageBuilder( + build: SQueuedBuild, + context: MessageBuilderContext +) extends MessageBuilder { + override def compile( + template: String, + setting: BuildSetting + ): SlackAttachment = { + + val text = """\{([\s\w-._%]+)}""".r.replaceAllIn( + template, + m => + m.group(1) match { + case "name" => + encodeText( + context.getQueuedBuildName(build) + ) // "" // encodeText(build.getFullName) + case "number" => "" // has no number yet + case "branch" => context.getQueuedBuildBranch(build) + case "status" => statusQueued + case "changes" => "" // changes is unknown now + case "allArtifactsDownloadUrl" => "" + case "artifactsRelUrl" => "" + case "artifactLinks" => "" + case "link" => context.getQueuedBuildUrl(build) + case "mentions" => "" // changes is unknown now + case "users" => "" // changes is unknown now + case "formattedDuration" => "" // build only queued + case "reason" => "" + case x if x.startsWith("%") && x.endsWith("%") => + context.getQueuedBuildParameter( + build, + x.substring(1, x.length - 1).trim + ) match { + case Some(value) => encodeText(value) + case _ => unknownParameter + } + case _ => m.group(0) } - case _ ⇒ m.group(0) - }) + ) SlackAttachment(text.trim, statusNormalColor, "⚪") } @@ -139,45 +210,61 @@ object SBuildMessageBuilder { |{mentions} """.stripMargin.trim - private def statusColor(status: Status) = if (status == Status.NORMAL) statusNormalColor else status.getHtmlColor + private def statusColor(status: Status) = + if (status == Status.NORMAL) statusNormalColor else status.getHtmlColor private def statusEmoji(status: Status) = status match { - case Status.NORMAL ⇒ "✅" - case Status.FAILURE ⇒ "⛔" - case _ ⇒ "⚪" + case Status.NORMAL => "✅" + case Status.FAILURE => "⛔" + case _ => "⚪" } - case class MessageBuilderContext(webLinks: WebLinks, gateway: SlackGateway, paths: ServerPaths, configManager: ConfigManager) { - def getViewResultsUrl: SBuild ⇒ String = webLinks.getViewResultsUrl + case class MessageBuilderContext( + webLinks: WebLinks, + gateway: SlackGateway, + paths: ServerPaths, + configManager: ConfigManager + ) { + def getViewResultsUrl: SBuild => String = webLinks.getViewResultsUrl - def getDownloadAllArtifactsUrl: SBuild ⇒ String = webLinks.getDownloadAllArtefactsUrl + def getDownloadAllArtifactsUrl: SBuild => String = + webLinks.getDownloadAllArtefactsUrl - def userByEmail: String ⇒ Option[String] = email ⇒ gateway.session.flatMap(s ⇒ Option(s.findUserByEmail(email))).map(_.getId) + def userByEmail: String => Option[String] = email => + gateway.getUserByEmail(email).map(_.getId) def getArtifactsPath: String = paths.getArtifactsDirectory.getPath def artifactsPublicUrl: Option[String] = configManager.publicUrl - def getBuildParameter: (SBuild, String) ⇒ Option[String] = (build, name) ⇒ + def getBuildParameter: (SBuild, String) => Option[String] = (build, name) => Option(build.getParametersProvider.get(name)) - def getQueuedBuildName: SQueuedBuild => String = build => build.getBuildType.getFullName + def getQueuedBuildName: SQueuedBuild => String = build => + build.getBuildType.getFullName def getQueuedBuildUrl: SQueuedBuild => String = webLinks.getQueuedBuildUrl def getQueuedBuildBranch: SQueuedBuild => String = build => - Option(build.getBuildPromotion.getBranch).map(_.getDisplayName).getOrElse(unknownBranch) - - def getQueuedBuildParameter: (SQueuedBuild, String) ⇒ Option[String] = (build, name) => - Option(build.getBuildPromotion.getParameterValue(name)) + Option(build.getBuildPromotion.getBranch) + .map(_.getDisplayName) + .getOrElse(unknownBranch) + def getQueuedBuildParameter: (SQueuedBuild, String) => Option[String] = + (build, name) => Option(build.getBuildPromotion.getParameterValue(name)) } } -class MessageBuilderFactory(webLinks: WebLinks, gateway: SlackGateway, paths: ServerPaths, configManager: ConfigManager) { - private val context = MessageBuilderContext(webLinks, gateway, paths, configManager) +class MessageBuilderFactory( + webLinks: WebLinks, + gateway: SlackGateway, + paths: ServerPaths, + configManager: ConfigManager +) { + private val context = + MessageBuilderContext(webLinks, gateway, paths, configManager) def createForBuild(build: SBuild) = new SBuildMessageBuilder(build, context) - def createForBuild(build: SQueuedBuild) = new SQueuedBuildMessageBuilder(build, context) + def createForBuild(build: SQueuedBuild) = + new SQueuedBuildMessageBuilder(build, context) } - diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/NotificationSender.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/NotificationSender.scala index 3d10bc3..f2c591c 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/NotificationSender.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/NotificationSender.scala @@ -2,10 +2,16 @@ package com.fpd.teamcity.slack import com.fpd.teamcity.slack.ConfigManager.BuildSetting import com.fpd.teamcity.slack.ConfigManager.BuildSettingFlag.BuildSettingFlag -import com.fpd.teamcity.slack.SlackGateway.{Destination, MessageSent, SlackChannel, SlackUser, attachmentToSlackMessage} +import com.fpd.teamcity.slack.SlackGateway.{ + Destination, + SlackChannel, + SlackUser, + attachmentToSlackMessage +} import jetbrains.buildServer.serverSide.{SBuild, SQueuedBuild} import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future trait NotificationSender { @@ -16,24 +22,27 @@ trait NotificationSender { import Helpers.Implicits._ - type SendResult = Vector[Future[MessageSent]] + type SendResult = Vector[Future[Unit]] - private def sendAsAttachment = configManager.sendAsAttachment.exists(x ⇒ x) + private def sendAsAttachment = configManager.sendAsAttachment.exists(x => x) - def send(build: SBuild, flags: Set[BuildSettingFlag]): Future[Vector[MessageSent]] = { + def send( + build: SBuild, + flags: Set[BuildSettingFlag] + ): Future[Vector[Unit]] = { val settings = prepareSettings(build, flags) lazy val emails = build.committeeEmails lazy val messageBuilder = messageBuilderFactory.createForBuild(build) lazy val sendPersonal = shouldSendPersonal(build) - val result = settings.foldLeft(Vector(): SendResult) { (acc, setting) ⇒ + val result = settings.foldLeft(Vector(): SendResult) { (acc, setting) => val attachment = messageBuilder.compile(setting.messageTemplate, setting) val destinations = mutable.Set.empty[Destination] if (build.isPersonal) { // If build is personal we need inform only build's owner if needed val email = build.getOwner.getEmail - if (sendPersonal && email.length > 0) { + if (sendPersonal && email.nonEmpty) { destinations += SlackUser(email) } } else { @@ -41,56 +50,71 @@ trait NotificationSender { destinations += SlackChannel(setting.slackChannel) } - /** - * if build fails all committees should receive the message + /** if build fails all committees should receive the message * if personal notification explicitly enabled in build settings let's notify all committees */ if (setting.notifyCommitter || sendPersonal) { - emails.foreach { email ⇒ + emails.foreach { email => destinations += SlackUser(email) } } } - acc ++ destinations.toVector.map(x ⇒ - gateway.sendMessage(x, attachmentToSlackMessage(attachment, sendAsAttachment)) + acc ++ destinations.toVector.map(x => + gateway.sendMessage( + x, + attachmentToSlackMessage(attachment, sendAsAttachment) + ) ) } - implicit val ec = scala.concurrent.ExecutionContext.global Future.sequence(result) } - def send(build: SQueuedBuild, flags: Set[BuildSettingFlag]): Future[Vector[MessageSent]] = { + def send( + build: SQueuedBuild, + flags: Set[BuildSettingFlag] + ): Future[Vector[Unit]] = { val settings = prepareSettings(build, flags) lazy val messageBuilder = messageBuilderFactory.createForBuild(build) - val result = settings.foldLeft(Vector(): SendResult) { (acc, setting) ⇒ + val result = settings.foldLeft(Vector(): SendResult) { (acc, setting) => val attachment = messageBuilder.compile(setting.messageTemplate, setting) val destinations = mutable.Set.empty[Destination] if (!build.isPersonal && setting.slackChannel.nonEmpty) { - destinations += SlackChannel(setting.slackChannel) + destinations += SlackChannel(setting.slackChannel) } - acc ++ destinations.toVector.map(x ⇒ - gateway.sendMessage(x, attachmentToSlackMessage(attachment, sendAsAttachment)) + acc ++ destinations.toVector.map(x => + gateway.sendMessage( + x, + attachmentToSlackMessage(attachment, sendAsAttachment) + ) ) } - implicit val ec = scala.concurrent.ExecutionContext.global Future.sequence(result) } - def shouldSendPersonal(build: SBuild): Boolean = build.getBuildStatus.isFailed && configManager.personalEnabled.exists(x ⇒ x) + def shouldSendPersonal(build: SBuild): Boolean = + build.getBuildStatus.isFailed && configManager.personalEnabled.exists(x => + x + ) - def prepareSettings(build: SBuild, flags: Set[BuildSettingFlag]): Iterable[BuildSetting] = - configManager.buildSettingList(build.getBuildTypeId).values.filter { x ⇒ + def prepareSettings( + build: SBuild, + flags: Set[BuildSettingFlag] + ): Iterable[BuildSetting] = + configManager.buildSettingList(build.getBuildTypeId).values.filter { x => x.pureFlags.intersect(flags).nonEmpty && build.matchBranch(x.branchMask) } - def prepareSettings(build: SQueuedBuild, flags: Set[BuildSettingFlag]): Iterable[BuildSetting] = - configManager.buildSettingList(build.getBuildTypeId).values.filter { x ⇒ + def prepareSettings( + build: SQueuedBuild, + flags: Set[BuildSettingFlag] + ): Iterable[BuildSetting] = + configManager.buildSettingList(build.getBuildTypeId).values.filter { x => x.pureFlags.intersect(flags).nonEmpty && build.matchBranch(x.branchMask) } } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/PermissionManager.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/PermissionManager.scala index 9ab3590..cd0374e 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/PermissionManager.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/PermissionManager.scala @@ -6,40 +6,54 @@ import jetbrains.buildServer.serverSide.ProjectManager import jetbrains.buildServer.serverSide.auth.{Permission, RoleEntry} import jetbrains.buildServer.users.SUser -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ import scala.language.implicitConversions class PermissionManager( - projectManager: ProjectManager, - configManager: ConfigManager - ) { + projectManager: ProjectManager, + configManager: ConfigManager +) { def accessPermitted(request: Request): Boolean = isAdmin(request) - def settingAccessPermitted(request: Request, settingId: String): Boolean = configManager.isAvailable && - (isAdmin(request) || request.exists(settingIdPermitted(_, settingId))) + def settingAccessPermitted(request: Request, settingId: String): Boolean = + configManager.isAvailable && + (isAdmin(request) || request.exists(settingIdPermitted(_, settingId))) - def buildAccessPermitted(request: Request, buildTypeId: String): Boolean = configManager.isAvailable && - (isAdmin(request) || request.exists(buildTypeIdPermitted(_, buildTypeId))) + def buildAccessPermitted(request: Request, buildTypeId: String): Boolean = + configManager.isAvailable && + (isAdmin(request) || request.exists(buildTypeIdPermitted(_, buildTypeId))) private def isAdmin(request: Request): Boolean = - request.exists(_.isPermissionGrantedGlobally(Permission.CHANGE_SERVER_SETTINGS)) + request.exists( + _.isPermissionGrantedGlobally(Permission.CHANGE_SERVER_SETTINGS) + ) - private def settingIdPermitted(user: SUser, settingId: String): Boolean = configManager.buildSetting(settingId) - .map(_.buildTypeId) - .exists(buildTypeIdPermitted(user, _)) + private def settingIdPermitted(user: SUser, settingId: String): Boolean = + configManager + .buildSetting(settingId) + .map(_.buildTypeId) + .exists(buildTypeIdPermitted(user, _)) private def buildTypeIdPermitted(user: SUser, buildTypeId: String): Boolean = - Some(projectManager.findProjectId(buildTypeId)).exists(isProjectAdmin(user, _)) + Some(projectManager.findProjectId(buildTypeId)) + .exists(isProjectAdmin(user, _)) private def isProjectAdmin(user: SUser, projectId: String): Boolean = { - lazy val parentProjects = projectManager.findProjectById(projectId).getProjectPath.asScala.map(_.getProjectId) + lazy val parentProjects = projectManager + .findProjectById(projectId) + .getProjectPath + .asScala + .map(_.getProjectId) lazy val directRoles = user.getRoles.asScala - lazy val parentRoles = user.getParentHolders.asScala.flatMap(_.getRoles.asScala) + lazy val parentRoles = + user.getParentHolders.asScala.flatMap(_.getRoles.asScala) def projectAdmin(entry: RoleEntry): Boolean = - entry.getRole.getId == "PROJECT_ADMIN" && parentProjects.contains(entry.getScope.getProjectId) + entry.getRole.getId == "PROJECT_ADMIN" && parentProjects.contains( + entry.getScope.getProjectId + ) directRoles.exists(projectAdmin) || parentRoles.exists(projectAdmin) } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Resources.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Resources.scala index b24e32d..6517eda 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Resources.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Resources.scala @@ -5,11 +5,11 @@ object Resources { case class View(view: String) case class Action(url: String) { - lazy val controllerUrl = withLeadingSlash(url) + lazy val controllerUrl: String = withLeadingSlash(url) } case class Page(url: String, view: String) { - lazy val controllerUrl = withLeadingSlash(url) + lazy val controllerUrl: String = withLeadingSlash(url) } private def withLeadingSlash(url: String) = { @@ -17,12 +17,25 @@ object Resources { else s"/$url" } - lazy val buildPage = View("buildPage.jsp") - lazy val buildSettingList = Page("app/slackIntegration/buildSettingList.html", "buildSettingListPage.jsp") - lazy val buildSettingEdit = Page("app/slackIntegration/buildSettingEdit.html", "buildSettingEditPage.jsp") - lazy val buildSettingSave = Action("app/slackIntegration/buildSettingSave.html") - lazy val buildSettingDelete = Action("app/slackIntegration/buildSettingDelete.html") - lazy val buildSettingTry = Action("app/slackIntegration/buildSettingTry.html") - lazy val configPage = Page("app/slackIntegration/config", "configPage.jsp") - lazy val ajaxView = View("ajaxView.jsp") + lazy val buildPage: View = View("buildPage.jsp") + lazy val buildSettingList: Page = Page( + "app/slackIntegration/buildSettingList.html", + "buildSettingListPage.jsp" + ) + lazy val buildSettingEdit: Page = Page( + "app/slackIntegration/buildSettingEdit.html", + "buildSettingEditPage.jsp" + ) + lazy val buildSettingSave: Action = Action( + "app/slackIntegration/buildSettingSave.html" + ) + lazy val buildSettingDelete: Action = Action( + "app/slackIntegration/buildSettingDelete.html" + ) + lazy val buildSettingTry: Action = Action( + "app/slackIntegration/buildSettingTry.html" + ) + lazy val configPage: Page = + Page("app/slackIntegration/config", "configPage.jsp") + lazy val ajaxView: View = View("ajaxView.jsp") } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackGateway.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackGateway.scala index 2bd2f3f..a00cd31 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackGateway.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackGateway.scala @@ -1,15 +1,24 @@ package com.fpd.teamcity.slack -import java.net.Proxy -import java.util.concurrent.TimeUnit - import com.fpd.teamcity.slack.Strings.SlackGateway._ -import com.ullink.slack.simpleslackapi.impl.SlackSessionFactory -import com.ullink.slack.simpleslackapi.replies.{ParsedSlackReply, SlackMessageReply} -import com.ullink.slack.simpleslackapi.{SlackChatConfiguration, SlackMessageHandle, SlackSession, SlackAttachment ⇒ ApiSlackAttachment} +import com.slack.api.methods.request.chat.ChatPostMessageRequest +import com.slack.api.methods.request.conversations.ConversationsListRequest +import com.slack.api.methods.request.users.UsersLookupByEmailRequest +import com.slack.api.methods.response.chat.ChatPostMessageResponse +import com.slack.api.methods.{ + AsyncMethodsClient, + MethodsClient, + SlackApiTextResponse +} +import com.slack.api.model.{Attachment, ConversationType, Field, User} +import com.slack.api.{Slack, SlackConfig} import jetbrains.buildServer.serverSide.TeamCityProperties -import scala.concurrent.{ExecutionContextExecutor, Future} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.jdk.CollectionConverters._ +import scala.jdk.FutureConverters._ import scala.language.{implicitConversions, postfixOps} import scala.util.{Failure, Success, Try} @@ -25,21 +34,32 @@ object SlackGateway { override def toString: String = s"#$name" } - case class SlackMessage(message: String, attachment: Option[ApiSlackAttachment] = None) { + case class SlackMessage( + message: String, + attachment: Option[Attachment] = None + ) { lazy val isEmpty: Boolean = message.isEmpty && attachment.isEmpty + def attachmentsList: Seq[Attachment] = attachment match { + case Some(attach) => Seq(attach) + case _ => Seq() + } } case class SlackAttachment(text: String, color: String, emoji: String) - implicit def stringToSlackMessage(message: String): SlackMessage = SlackMessage(message) + implicit def stringToSlackMessage(message: String): SlackMessage = + SlackMessage(message) - type MessageSent = Try[SlackMessageHandle[SlackMessageReply]] - - def attachmentToSlackMessage(attachment: SlackAttachment, asAttachment: Boolean): SlackMessage = if (asAttachment) { - val apiSlackAttachment = new ApiSlackAttachment() + def attachmentToSlackMessage( + attachment: SlackAttachment, + asAttachment: Boolean + ): SlackMessage = if (asAttachment) { + val apiSlackAttachment = new Attachment() apiSlackAttachment.setColor(attachment.color) - apiSlackAttachment.addMarkdownIn("fields") - apiSlackAttachment.addField("", attachment.text, false) + apiSlackAttachment.setMrkdwnIn(Seq("fields").asJava) + apiSlackAttachment.setFields( + Seq(new Field("", attachment.text, false)).asJava + ) SlackMessage("", Some(apiSlackAttachment)) } else { @@ -53,7 +73,8 @@ object SlackGateway { } def getStringProperty(key: String): Option[String] = { - Try(Option(System.getProperty(key))).getOrElse(None) + Try(Option(System.getProperty(key))) + .getOrElse(None) .trimEmptyString .orElse( Option(TeamCityProperties.getPropertyOrNull(s"teamcity.$key")) @@ -68,110 +89,192 @@ object SlackGateway { ) case class SendMessageError(message: String) extends Exception(message) + + case class SlackApiError(message: String) extends Exception(message) + + private def prepareConfig: SlackConfig = { + val config = new SlackConfig + + val proxyHost = getStringProperty("https.proxyHost") + val proxyPort = getIntProperty("https.proxyPort") + + val proxyUrl = proxyHost.map(host => + if (proxyPort > 0) s"http://$host:$proxyPort" + else s"http://$host" + ) + + proxyUrl.foreach(x => config.setProxyUrl(x)) + + for { + url <- proxyUrl + proxyLogin <- getStringProperty("https.proxyLogin") + proxyPassword <- getStringProperty("https.proxyPassword") + } yield config.setProxyUrl(s"http://$proxyLogin:$proxyPassword@$url") + + config + } + + private def processResult[T <: SlackApiTextResponse]( + result: Future[T] + ): Future[T] = result.flatMap(response => { + if (response.isOk) Future.successful(response) + else Future.failed(SlackApiError(response.getError)) + }) } class SlackGateway(val configManager: ConfigManager, logger: Logger) { import SlackGateway._ - var sessions = Map.empty[String, SlackSession] + private val slack = Slack.getInstance(prepareConfig) - lazy private val proxyHost = getStringProperty("https.proxyHost") - lazy private val proxyPort = getIntProperty("https.proxyPort") - lazy private val proxyLogin = getStringProperty("https.proxyLogin") - lazy private val proxyPassword = getStringProperty("https.proxyPassword") + var methodsClients = Map.empty[String, AsyncMethodsClient] - def session: Option[SlackSession] = configManager.config.flatMap(x ⇒ sessionByConfig(x).toOption) + private def methods: Option[AsyncMethodsClient] = + configManager.config.flatMap(x => sessionByConfig(x)) - def sessionByConfig(config: ConfigManager.Config): Try[SlackSession] = sessions.get(config.oauthKey).filter(_.isConnected) match { - case Some(x) ⇒ Success(x) - case _ ⇒ - val session = if (proxyHost.isDefined) - SlackSessionFactory - .getSlackSessionBuilder(config.oauthKey) - .withAutoreconnectOnDisconnection(true) - .withConnectionHeartbeat(0, null) - .withProxy(Proxy.Type.HTTP, proxyHost.get, proxyPort, proxyLogin.orNull, proxyPassword.orNull) - .build() - else - SlackSessionFactory.createWebSocketSlackSession(config.oauthKey) + private def checkConnection(methods: MethodsClient): Boolean = { + val request = + ConversationsListRequest + .builder() + .limit(1) + .build() - val option = Try(session.connect()).map(_ ⇒ session) - option.foreach(s ⇒ sessions = sessions + (config.oauthKey → s)) - option + Try(methods.conversationsList(request)) match { + case Success(value) if value.isOk => true + case Success(value) => + logger.log(value.getError) + false + case Failure(exception) => + logger.log(exception.getMessage) + false + } } - def sendMessage(destination: Destination, message: SlackMessage): Future[MessageSent] = + def sessionByConfig( + config: ConfigManager.Config + ): Option[AsyncMethodsClient] = + methodsClients.get(config.oauthKey) match { + case None => + val syncMethods = slack.methods(config.oauthKey) + + if (checkConnection(syncMethods)) { + val methods = slack.methodsAsync(config.oauthKey) + methodsClients = methodsClients + (config.oauthKey -> methods) + + Some(methods) + } else { + None + } + case x => x + } + + def sendMessage( + destination: Destination, + message: SlackMessage + ): Future[Unit] = if (message.isEmpty) { logger.log("Empty message") - Future.successful(Failure(SendMessageError("Empty message"))) + Future.failed(SendMessageError("Empty message")) } else { - implicit val ec: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global - Future { - val handle = sendMessageInternal(destination, message) - handle.foreach(_.waitForReply(networkTimeout, TimeUnit.SECONDS)) - handle - } transform ( - result ⇒ processResult(destination, result), - exception ⇒ { - logger.log(exception.toString) - exception - } - ) + methods match { + case Some(client) => + val response = sendMessageInternal(client, destination, message) + + processResult(response).transform( + _ => logger.log(messageSent(destination)), + throwable => { + val message = + failedToSendToDestination(destination, throwable.getMessage) + logger.log(message) + + SendMessageError(message) + } + ) + case None => + Future.failed(SendMessageError(emptySession)) + } } - private def channelChatConfiguration = - configManager.senderName match { - case Some(senderName) ⇒ - SlackChatConfiguration.getConfiguration.withName(senderName) - case _ ⇒ - SlackChatConfiguration.getConfiguration.asUser() + def getUserByEmail(email: String): Option[User] = methods.flatMap { client => + { + val request = UsersLookupByEmailRequest.builder().email(email).build() + + val response = client.usersLookupByEmail(request).asScala + + val user = processResult(response).map(_.getUser) + + Try(Await.result(user, 10 seconds)).fold( + exception => { + logger.log(exception.getMessage) + None + }, + response => Some(response) + ) } + } - private def sendMessageInternal(destination: Destination, message: SlackMessage): MessageSent = session match { - case Some(x) ⇒ - destination match { - case SlackChannel(channelName) ⇒ - Option(x.findChannelByName(channelName)) match { - case Some(channel) ⇒ - Try(x.sendMessage(channel, message.message, message.attachment.orNull, channelChatConfiguration)) - case _ ⇒ - Failure(SendMessageError(channelNotFound(channelName))) - } - case SlackUser(email) ⇒ - Option(x.findUserByEmail(email)) match { - case Some(user) ⇒ - Try(x.sendMessageToUser(user, message.message, message.attachment.orNull)) - case _ ⇒ - Failure(SendMessageError(userNotFound(email))) - } - case _ ⇒ - Failure(SendMessageError(unknownDestination)) + def isChannelExists(channel: String): Option[Boolean] = methods.map { + client => + { + val types = + Seq(ConversationType.PUBLIC_CHANNEL, ConversationType.PRIVATE_CHANNEL) + + val request = + ConversationsListRequest + .builder() + .limit(1000) + .excludeArchived(true) + .types(types.asJava) + .build() + + val response = client.conversationsList(request).asScala + + val channels = processResult(response).map(_.getChannels.asScala) + + Try(Await.result(channels, 30 seconds)).fold( + exception => { + logger.log(exception.getMessage) + false + }, + response => response.exists(_.getName == channel) + ) } - case _ ⇒ - Failure(SendMessageError(emptySession)) } - private def processResult(destination: Destination, result: MessageSent): MessageSent = { - result match { - case Success(sent) if sent.getReply != null ⇒ - parseReplyError(sent.getReply) match { - case Some(error) ⇒ - val message = failedToSendToDestination(destination, error) - logger.log(message) - Failure(SendMessageError(message)) - case _ ⇒ - logger.log(messageSent(destination)) - Success(sent) + private def sendMessageInternal( + client: AsyncMethodsClient, + destination: Destination, + message: SlackMessage + ): Future[ChatPostMessageResponse] = { + val requestBuilder = ChatPostMessageRequest + .builder() + .text(message.message) + .attachments(message.attachmentsList.asJava) + + val channelName = destination match { + case SlackChannel(channelName) => + // change sender name for channel only + requestBuilder.username(configManager.senderName.orNull) + + Right(channelName) + case SlackUser(email) => + getUserByEmail(email).map(_.getId) match { + case Some(value) => Right(value) + case None => + Left(userNotFound(email)) } - case x @ Failure(reason) ⇒ - logger.log(reason.getMessage) - x + case _ => Left(unknownDestination) } - } - private def parseReplyError(reply: ParsedSlackReply): Option[String] = - if (!reply.isOk) { - Some(reply.getErrorMessage) - } else None + channelName match { + case Right(value) => + val request = requestBuilder + .channel(value) + .build() + + client.chatPostMessage(request).asScala + case Left(error) => Future.failed(SendMessageError(error)) + } + } } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackNotifier.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackNotifier.scala index 07a721a..74b2de1 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackNotifier.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackNotifier.scala @@ -2,18 +2,25 @@ package com.fpd.teamcity.slack import java.util -import jetbrains.buildServer.notification.{NotificatorAdapter, NotificatorRegistry} +import jetbrains.buildServer.notification.{ + NotificatorAdapter, + NotificatorRegistry +} import jetbrains.buildServer.serverSide.SRunningBuild import jetbrains.buildServer.users.{NotificatorPropertyKey, SUser} import scala.language.implicitConversions -class SlackNotifier(notificatorRegistry: NotificatorRegistry) extends NotificatorAdapter { +class SlackNotifier(notificatorRegistry: NotificatorRegistry) + extends NotificatorAdapter { import SlackNotifier._ notificatorRegistry.register(this) - override def notifyBuildStarted(build: SRunningBuild, users: util.Set[SUser]): Unit = Unit + override def notifyBuildStarted( + build: SRunningBuild, + users: util.Set[SUser] + ): Unit = () override def getNotificatorType: String = notificatorType @@ -21,7 +28,9 @@ class SlackNotifier(notificatorRegistry: NotificatorRegistry) extends Notificato } object SlackNotifier { - implicit private def propertyNameToPropertyKey(propertyName: String): NotificatorPropertyKey = + implicit private def propertyNameToPropertyKey( + propertyName: String + ): NotificatorPropertyKey = new NotificatorPropertyKey(notificatorType, propertyName) private def notificatorType = Strings.tabId diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackServerAdapter.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackServerAdapter.scala index 3f95b78..5571a2b 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackServerAdapter.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/SlackServerAdapter.scala @@ -4,19 +4,28 @@ import com.fpd.teamcity.slack.ConfigManager.BuildSettingFlag import com.fpd.teamcity.slack.ConfigManager.BuildSettingFlag.BuildSettingFlag import com.fpd.teamcity.slack.Helpers.Implicits._ import jetbrains.buildServer.messages.Status -import jetbrains.buildServer.serverSide.{BuildServerAdapter, SBuildServer, SQueuedBuild, SRunningBuild} +import jetbrains.buildServer.serverSide.{ + BuildServerAdapter, + SBuildServer, + SQueuedBuild, + SRunningBuild +} -class SlackServerAdapter(sBuildServer: SBuildServer, - val configManager: ConfigManager, - val gateway: SlackGateway, - val messageBuilderFactory: MessageBuilderFactory - ) extends BuildServerAdapter with NotificationSender { +class SlackServerAdapter( + sBuildServer: SBuildServer, + val configManager: ConfigManager, + val gateway: SlackGateway, + val messageBuilderFactory: MessageBuilderFactory +) extends BuildServerAdapter + with NotificationSender { import SlackServerAdapter._ sBuildServer.addListener(this) - override def buildFinished(build: SRunningBuild): Unit = if (configManager.isAvailable) { + override def buildFinished(build: SRunningBuild): Unit = if ( + configManager.isAvailable + ) { val previousStatus = sBuildServer.findPreviousStatus(build) val flags = calculateFlags(previousStatus, build.getBuildStatus) @@ -25,23 +34,33 @@ class SlackServerAdapter(sBuildServer: SBuildServer, } } - override def buildStarted(build: SRunningBuild): Unit = if (configManager.isAvailable) + override def buildStarted(build: SRunningBuild): Unit = if ( + configManager.isAvailable + ) send(build, Set(BuildSettingFlag.started)) - override def buildInterrupted(build: SRunningBuild): Unit = if (configManager.isAvailable) + override def buildInterrupted(build: SRunningBuild): Unit = if ( + configManager.isAvailable + ) send(build, Set(BuildSettingFlag.canceled)) - override def buildTypeAddedToQueue(build: SQueuedBuild): Unit = if (configManager.isAvailable) + override def buildTypeAddedToQueue(build: SQueuedBuild): Unit = if ( + configManager.isAvailable + ) send(build, Set(BuildSettingFlag.queued)) } object SlackServerAdapter { - def calculateFlags(previous: Status, current: Status): Set[BuildSettingFlag] = { + def calculateFlags( + previous: Status, + current: Status + ): Set[BuildSettingFlag] = { import BuildSettingFlag._ def changed = statusChanged(previous, current) - def applyIfChanged(flag1: BuildSettingFlag, flag2: BuildSettingFlag) = if (changed) Set(flag1, flag2) else Set(flag1) + def applyIfChanged(flag1: BuildSettingFlag, flag2: BuildSettingFlag) = + if (changed) Set(flag1, flag2) else Set(flag1) if (current.isSuccessful) { applyIfChanged(success, failureToSuccess) diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Strings.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Strings.scala index 70b65f0..4e77fee 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Strings.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/Strings.scala @@ -7,7 +7,8 @@ object Strings { def label: String = "Slack" def tabId: String = "Slack" - private def unableToCreateSessionByConfig(reason: String): String = s"Unable to create session by config: $reason" + private def unableToCreateSessionByConfig(reason: String): String = + s"Unable to create session by config: $reason" object MessageBuilder { lazy val unknownBranch = "Unknown" @@ -21,18 +22,22 @@ object Strings { } object BuildSettingsController { - lazy val channelOrNotifyCommitterError = "Either specify Slack channel name or check Notify committer flag" + lazy val channelOrNotifyCommitterError = + "Either specify Slack channel name or check Notify committer flag" lazy val compileBranchMaskError = "Unable to compile branch mask" lazy val compileArtifactsMaskError = "Unable to compile artifacts mask" - def sessionByConfigError(reason: String): String = unableToCreateSessionByConfig(reason) - def channelNotFoundError(channel: String): String = s"Unable to find channel with name $channel" + def sessionByConfigError(reason: String): String = + unableToCreateSessionByConfig(reason) + def channelNotFoundError(channel: String): String = + s"Unable to find channel with name $channel" lazy val emptyConfigError = "Config is empty" lazy val requirementsError = "One or more required params are missing" } object ConfigController { lazy val oauthTokenUpdateFailed = "Failed to update OAuth Access Token" - def sessionByConfigError(reason: String): String = unableToCreateSessionByConfig(reason) + def sessionByConfigError(reason: String): String = + unableToCreateSessionByConfig(reason) lazy val oauthKeyParamMissing = "Param oauthKey is missing" } @@ -45,11 +50,13 @@ object Strings { } object SlackGateway { - def failedToSendToDestination(destination: Destination, error: String) = s"Message to $destination wasn't sent. Reason: $error" + def failedToSendToDestination(destination: Destination, error: String) = + s"Message to $destination wasn't sent. Reason: $error" def messageSent(destination: Destination) = s"Message sent to $destination" def channelNotFound(channel: String) = s"Channel #$channel not found" def userNotFound(email: String) = s"User for $email not found" lazy val unknownDestination = "Destination is unknown" - lazy val emptySession = "Unable to connect your Slack account. Please check auth credentials" + lazy val emptySession = + "Unable to connect your Slack account. Please check auth credentials" } } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsDelete.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsDelete.scala index 586848c..c506e93 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsDelete.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsDelete.scala @@ -4,27 +4,40 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import com.fpd.teamcity.slack.Helpers.Implicits._ import com.fpd.teamcity.slack.{ConfigManager, PermissionManager, Resources} -import jetbrains.buildServer.web.openapi.{PluginDescriptor, WebControllerManager} +import jetbrains.buildServer.web.openapi.{ + PluginDescriptor, + WebControllerManager +} import org.springframework.web.servlet.ModelAndView -class BuildSettingsDelete(configManager: ConfigManager, - controllerManager: WebControllerManager, - val permissionManager: PermissionManager, - implicit val descriptor: PluginDescriptor - ) - extends SlackController { +class BuildSettingsDelete( + configManager: ConfigManager, + controllerManager: WebControllerManager, + val permissionManager: PermissionManager, + implicit val descriptor: PluginDescriptor +) extends SlackController { - controllerManager.registerController(Resources.buildSettingDelete.controllerUrl, this) + controllerManager.registerController( + Resources.buildSettingDelete.controllerUrl, + this + ) - override def handle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView = { + override def handle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView = { val result = for { - id ← request.param("id") - result ← configManager.removeBuildSetting(id) + id <- request.param("id") + result <- configManager.removeBuildSetting(id) } yield result - ajaxView(result.filter(_ == true).map(_ ⇒ "") getOrElse "Something went wrong") + ajaxView( + result.filter(_ == true).map(_ => "") getOrElse "Something went wrong" + ) } override protected def checkPermission(request: HttpServletRequest): Boolean = - request.param("id").exists(id ⇒ permissionManager.settingAccessPermitted(request, id)) + request + .param("id") + .exists(id => permissionManager.settingAccessPermitted(request, id)) } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSave.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSave.scala index 7a2d202..ec25373 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSave.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSave.scala @@ -1,83 +1,106 @@ package com.fpd.teamcity.slack.controllers -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} - import com.fpd.teamcity.slack.ConfigManager.{BuildSetting, BuildSettingFlag} import com.fpd.teamcity.slack.Helpers.Implicits._ import com.fpd.teamcity.slack.Strings.BuildSettingsController._ -import com.fpd.teamcity.slack.{ConfigManager, PermissionManager, Resources, SlackGateway} -import jetbrains.buildServer.web.openapi.{PluginDescriptor, WebControllerManager} +import com.fpd.teamcity.slack.{ + ConfigManager, + PermissionManager, + Resources, + SlackGateway +} +import jetbrains.buildServer.web.openapi.{ + PluginDescriptor, + WebControllerManager +} import org.springframework.web.servlet.ModelAndView -import scala.util.{Failure, Success, Try} +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import scala.util.Try -class BuildSettingsSave(val configManager: ConfigManager, - controllerManager: WebControllerManager, - slackGateway: SlackGateway, - val permissionManager: PermissionManager, - implicit val descriptor: PluginDescriptor - ) - extends SlackController { +class BuildSettingsSave( + val configManager: ConfigManager, + controllerManager: WebControllerManager, + slackGateway: SlackGateway, + val permissionManager: PermissionManager, + implicit val descriptor: PluginDescriptor +) extends SlackController { - controllerManager.registerController(Resources.buildSettingSave.controllerUrl, this) + controllerManager.registerController( + Resources.buildSettingSave.controllerUrl, + this + ) - override def handle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView = + override def handle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView = ajaxView(handleSave(request)) def handleSave(request: HttpServletRequest): String = { def flags = { val keyToFlag = Map( - "success" → BuildSettingFlag.success, - "failureToSuccess" → BuildSettingFlag.failureToSuccess, - "fail" → BuildSettingFlag.failure, - "successToFailure" → BuildSettingFlag.successToFailure, - "started" → BuildSettingFlag.started, - "canceled" → BuildSettingFlag.canceled, - "queued" → BuildSettingFlag.queued + "success" -> BuildSettingFlag.success, + "failureToSuccess" -> BuildSettingFlag.failureToSuccess, + "fail" -> BuildSettingFlag.failure, + "successToFailure" -> BuildSettingFlag.successToFailure, + "started" -> BuildSettingFlag.started, + "canceled" -> BuildSettingFlag.canceled, + "queued" -> BuildSettingFlag.queued ) - val keys = keyToFlag.keys.filter(key ⇒ request.param(key).isDefined) + val keys = keyToFlag.keys.filter(key => request.param(key).isDefined) keys.map(keyToFlag).toSet } val result = for { - // preparing params - branch ← request.param("branchMask") - buildId ← request.param("buildTypeId") - message ← request.param("messageTemplate") - - config ← configManager.config + // preparing params + branch <- request.param("branchMask") + buildId <- request.param("buildTypeId") + message <- request.param("messageTemplate") } yield { lazy val artifactsMask = request.param("artifactsMask") - val channel = request.param("slackChannel") + val channel = request.param("slackChannel").getOrElse("") val notifyCommitter = request.param("notifyCommitter").isDefined - val maxVcsChanges = request.param("maxVcsChanges").getOrElse(BuildSetting.defaultMaxVCSChanges.toString).toInt + val maxVcsChanges = request + .param("maxVcsChanges") + .getOrElse(BuildSetting.defaultMaxVCSChanges.toString) + .toInt // store build setting def updateConfig() = configManager.updateBuildSetting( - BuildSetting(buildId, branch, channel.getOrElse(""), message, flags, artifactsMask.getOrElse(""), request.param("deepLookup").isDefined, notifyCommitter, maxVcsChanges), + BuildSetting( + buildId, + branch, + channel, + message, + flags, + artifactsMask.getOrElse(""), + request.param("deepLookup").isDefined, + notifyCommitter, + maxVcsChanges + ), request.param("key") ) // check channel availability - if (!channel.exists(_.nonEmpty) && !notifyCommitter) { + if (channel.isEmpty && !notifyCommitter) { channelOrNotifyCommitterError } else if (Try(branch.r).isFailure) { compileBranchMaskError - } else if (artifactsMask.exists(s ⇒ Try(s.r).isFailure)) { + } else if (artifactsMask.exists(s => Try(s.r).isFailure)) { compileArtifactsMaskError } else { - slackGateway.sessionByConfig(config) match { - case Success(session) ⇒ - if (channel.exists(s ⇒ null == session.findChannelByName(s))) { - channelNotFoundError(channel.get) - } else { - updateConfig() match { - case Some(_) ⇒ "" - case _ ⇒ emptyConfigError - } - } - case Failure(e) ⇒ - sessionByConfigError(e.getMessage) + if ( + channel.nonEmpty && !slackGateway + .isChannelExists(channel) + .getOrElse(false) + ) { + channelNotFoundError(channel) + } else { + updateConfig() match { + case Some(_) => "" + case _ => emptyConfigError + } } } } @@ -86,5 +109,7 @@ class BuildSettingsSave(val configManager: ConfigManager, } override protected def checkPermission(request: HttpServletRequest): Boolean = - request.param("buildTypeId").exists(permissionManager.buildAccessPermitted(request, _)) + request + .param("buildTypeId") + .exists(permissionManager.buildAccessPermitted(request, _)) } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTry.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTry.scala index e23ca29..06a1977 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTry.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTry.scala @@ -2,93 +2,129 @@ package com.fpd.teamcity.slack.controllers import com.fpd.teamcity.slack.ConfigManager.BuildSetting import com.fpd.teamcity.slack.Helpers.Implicits._ -import com.fpd.teamcity.slack.SlackGateway.{Destination, SlackChannel, SlackUser, attachmentToSlackMessage} +import com.fpd.teamcity.slack.SlackGateway.{ + Destination, + SlackChannel, + SlackUser, + attachmentToSlackMessage +} import com.fpd.teamcity.slack._ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import jetbrains.buildServer.serverSide.{ProjectManager, SFinishedBuild} import jetbrains.buildServer.users.SUser -import jetbrains.buildServer.web.openapi.{PluginDescriptor, WebControllerManager} +import jetbrains.buildServer.web.openapi.{ + PluginDescriptor, + WebControllerManager +} import jetbrains.buildServer.web.util.SessionUser import org.springframework.web.servlet.ModelAndView import scala.annotation.tailrec -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ import scala.concurrent.Await import scala.concurrent.duration._ import scala.language.postfixOps import scala.util.{Failure, Try} -class BuildSettingsTry(projectManager: ProjectManager, - configManager: ConfigManager, - gateway: SlackGateway, - controllerManager: WebControllerManager, - val permissionManager: PermissionManager, - messageBuilderFactory: MessageBuilderFactory, - implicit val descriptor: PluginDescriptor - ) - extends SlackController { +class BuildSettingsTry( + projectManager: ProjectManager, + configManager: ConfigManager, + gateway: SlackGateway, + controllerManager: WebControllerManager, + val permissionManager: PermissionManager, + messageBuilderFactory: MessageBuilderFactory, + implicit val descriptor: PluginDescriptor +) extends SlackController { import BuildSettingsTry._ import Strings.BuildSettingsTry._ - controllerManager.registerController(Resources.buildSettingTry.controllerUrl, this) + controllerManager.registerController( + Resources.buildSettingTry.controllerUrl, + this + ) - override def handle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView = Try { - val id = request.param("id") + override def handle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView = Try { + val id = request + .param("id") .getOrElse(throw HandlerException(emptyIdParam)) - val setting = configManager.buildSetting(id) + val setting = configManager + .buildSetting(id) .getOrElse(throw HandlerException(buildSettingNotFound)) val build = findPreviousBuild(projectManager, setting) .getOrElse(throw HandlerException(previousBuildNotFound)) detectDestination(setting, SessionUser.getUser(request)) match { - case Some(dest) ⇒ - val future = gateway.sendMessage(dest, + case Some(dest) => + val attachment = messageBuilderFactory + .createForBuild(build) + .compile(setting.messageTemplate, setting) + + val future = gateway.sendMessage( + dest, attachmentToSlackMessage( - messageBuilderFactory.createForBuild(build).compile(setting.messageTemplate, setting), - configManager.sendAsAttachment.exists(x ⇒ x) - )) - Await.result(future, 10 seconds) match { - case Failure(error) ⇒ throw HandlerException(error.getMessage) - case _ ⇒ messageSent(dest.toString) + attachment, + configManager.sendAsAttachment.exists(x => x) + ) + ) + + Try(Await.result(future, 30 seconds)) match { + case Failure(error) => throw HandlerException(error.getMessage) + case _ => messageSent(dest.toString) } - case _ ⇒ + case _ => throw HandlerException(unknownDestination) } - } recover { case x: HandlerException ⇒ s"Error: ${x.getMessage}" } map { + } recover { case x: HandlerException => s"Error: ${x.getMessage}" } map { ajaxView } get override protected def checkPermission(request: HttpServletRequest): Boolean = - request.param("id").exists(id ⇒ permissionManager.settingAccessPermitted(request, id)) + request + .param("id") + .exists(id => permissionManager.settingAccessPermitted(request, id)) } object BuildSettingsTry { @tailrec - def filterMatchBuild(setting: BuildSetting)(build: SFinishedBuild): Option[SFinishedBuild] = { + def filterMatchBuild( + setting: BuildSetting + )(build: SFinishedBuild): Option[SFinishedBuild] = { if (!build.isPersonal && build.matchBranch(setting.branchMask)) Some(build) else { Option(build.getPreviousFinished) match { - case Some(previous) ⇒ filterMatchBuild(setting)(previous) - case None ⇒ None + case Some(previous) => filterMatchBuild(setting)(previous) + case None => None } } } - def findPreviousBuild(projectManager: ProjectManager, setting: BuildSetting): Option[SFinishedBuild] = { - val buildTypes = projectManager.findBuildTypes(Vector(setting.buildTypeId).asJava) + def findPreviousBuild( + projectManager: ProjectManager, + setting: BuildSetting + ): Option[SFinishedBuild] = { + val buildTypes = + projectManager.findBuildTypes(Vector(setting.buildTypeId).asJava) val foundBuildType = buildTypes.asScala.headOption - foundBuildType.flatMap(buildType ⇒ filterMatchBuild(setting)(buildType.getLastChangesFinished)) + foundBuildType.flatMap(buildType => + filterMatchBuild(setting)(buildType.getLastChangesFinished) + ) } - def detectDestination(setting: BuildSetting, user: ⇒ SUser): Option[Destination] = setting.slackChannel.isEmpty match { - case true if setting.notifyCommitter ⇒ + def detectDestination( + setting: BuildSetting, + user: => SUser + ): Option[Destination] = setting.slackChannel.isEmpty match { + case true if setting.notifyCommitter => Some(SlackUser(user.getEmail)) - case false ⇒ + case false => Some(SlackChannel(setting.slackChannel)) - case _ ⇒ + case _ => None } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/ConfigController.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/ConfigController.scala index 65c44e2..a7299ca 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/ConfigController.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/ConfigController.scala @@ -1,48 +1,48 @@ package com.fpd.teamcity.slack.controllers -import java.net.URLEncoder -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} - -import com.fpd.teamcity.slack._ import com.fpd.teamcity.slack.Strings.ConfigController._ +import com.fpd.teamcity.slack._ import jetbrains.buildServer.web.openapi.WebControllerManager import org.springframework.web.servlet.ModelAndView -import scala.util.{Failure, Success} +import java.net.URLEncoder +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import scala.util.Try class ConfigController( - configManager: ConfigManager, - controllerManager: WebControllerManager, - val permissionManager: PermissionManager, - slackGateway: SlackGateway - ) - extends SlackController { + configManager: ConfigManager, + controllerManager: WebControllerManager, + val permissionManager: PermissionManager, + slackGateway: SlackGateway +) extends SlackController { import ConfigController._ import Helpers.Implicits._ controllerManager.registerController(Resources.configPage.controllerUrl, this) - override def handle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView = { + override def handle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView = { val result = for { - oauthKey ← request.param("oauthKey") + oauthKey <- request.param("oauthKey") } yield { val newConfig = ConfigManager.Config(oauthKey) val publicUrl = request.param("publicUrl").getOrElse("") val senderName = request.param("senderName").getOrElse("") - slackGateway.sessionByConfig(newConfig).map { _ ⇒ - configManager.update( - oauthKey, - publicUrl, - request.param("personalEnabled").isDefined, - request.param("enabled").isDefined, - senderName, - request.param("sendAsAttachment").isDefined - ) - } match { - case Success(true) ⇒ Left(true) - case Success(_) ⇒ Right(oauthTokenUpdateFailed) - case Failure(e) ⇒ Right(sessionByConfigError(e.getMessage)) + slackGateway.sessionByConfig(newConfig) match { + case Some(_) => + configManager.update( + oauthKey, + publicUrl, + request.param("personalEnabled").isDefined, + request.param("enabled").isDefined, + senderName, + request.param("sendAsAttachment").isDefined + ) + Left(true) + case _ => Right(sessionByConfigError("auth error")) } } @@ -53,8 +53,13 @@ class ConfigController( } object ConfigController { - private def createRedirect(either: Either[Boolean, String], context: String): String = either match { - case Left(_) ⇒ s"$context/admin/admin.html?item=${Strings.tabId}" - case Right(error) ⇒ s"$context/admin/admin.html?item=${Strings.tabId}&error=${URLEncoder.encode(error, "UTF-8")}" + private def createRedirect( + either: Either[Boolean, String], + context: String + ): String = either match { + case Left(_) => s"$context/admin/admin.html?item=${Strings.tabId}" + case Right(error) => + s"$context/admin/admin.html?item=${Strings.tabId}&error=${URLEncoder + .encode(error, "UTF-8")}" } } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/SlackController.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/SlackController.scala index c7373b2..ee7701c 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/SlackController.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/controllers/SlackController.scala @@ -10,19 +10,30 @@ import org.springframework.web.servlet.ModelAndView trait SlackController extends BaseController { protected val permissionManager: PermissionManager - def handle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView + def handle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView - override def doHandle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView = + override def doHandle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView = if (checkPermission(request)) handle(request, response) else SimpleView.createTextView("Access denied") - def ajaxView(message: String)(implicit descriptor: PluginDescriptor): ModelAndView = { - val modelAndView = new ModelAndView(descriptor.getPluginResourcesPath(Resources.ajaxView.view)) + def ajaxView( + message: String + )(implicit descriptor: PluginDescriptor): ModelAndView = { + val modelAndView = new ModelAndView( + descriptor.getPluginResourcesPath(Resources.ajaxView.view) + ) modelAndView.getModel.put("message", message) modelAndView } - protected def checkPermission(request: HttpServletRequest): Boolean = permissionManager.accessPermitted(request) + protected def checkPermission(request: HttpServletRequest): Boolean = + permissionManager.accessPermitted(request) } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildPage.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildPage.scala index b1bceaf..9708b2d 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildPage.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildPage.scala @@ -3,35 +3,44 @@ package com.fpd.teamcity.slack.pages import java.util import javax.servlet.http.HttpServletRequest -import com.fpd.teamcity.slack.{ConfigManager, PermissionManager, Resources, Strings} +import com.fpd.teamcity.slack.{PermissionManager, Resources, Strings} import jetbrains.buildServer.serverSide.{ProjectManager, SBuildType} import jetbrains.buildServer.users.SUser import jetbrains.buildServer.web.openapi.buildType.BuildTypeTab -import jetbrains.buildServer.web.openapi.{PluginDescriptor, WebControllerManager} +import jetbrains.buildServer.web.openapi.{ + PluginDescriptor, + WebControllerManager +} class BuildPage( - manager: WebControllerManager, - projectManager: ProjectManager, - descriptor: PluginDescriptor, - configManager: ConfigManager, - val permissionManager: PermissionManager - ) - extends - BuildTypeTab( + manager: WebControllerManager, + projectManager: ProjectManager, + descriptor: PluginDescriptor, + val permissionManager: PermissionManager +) extends BuildTypeTab( Strings.tabId, Strings.label, manager: WebControllerManager, projectManager: ProjectManager, - descriptor.getPluginResourcesPath(Resources.buildPage.view)) + descriptor.getPluginResourcesPath(Resources.buildPage.view) + ) with SlackExtension { addCssFile(descriptor.getPluginResourcesPath("css/slack-notifier.css")) addJsFile(descriptor.getPluginResourcesPath("js/slack-notifier.js")) override def isAvailable(request: HttpServletRequest): Boolean = - permissionManager.buildAccessPermitted(request, getBuildType(request).getInternalId) + permissionManager.buildAccessPermitted( + request, + getBuildType(request).getInternalId + ) - override def fillModel(model: util.Map[String, AnyRef], request: HttpServletRequest, buildType: SBuildType, user: SUser): Unit = { + override def fillModel( + model: util.Map[String, AnyRef], + request: HttpServletRequest, + buildType: SBuildType, + user: SUser + ): Unit = { model.put("buildTypeId", buildType.getBuildTypeId) model.put("buildSettingListUrl", Resources.buildSettingList.url) model.put("buildSettingEditUrl", Resources.buildSettingEdit.url) diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingEditPage.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingEditPage.scala index f0e3f2c..5dfb4a2 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingEditPage.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingEditPage.scala @@ -1,37 +1,57 @@ package com.fpd.teamcity.slack.pages -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} - import com.fpd.teamcity.slack.Helpers.Implicits._ import com.fpd.teamcity.slack.controllers.SlackController -import com.fpd.teamcity.slack.{ConfigManager, SBuildMessageBuilder, PermissionManager, Resources} -import jetbrains.buildServer.web.openapi.{PluginDescriptor, WebControllerManager} +import com.fpd.teamcity.slack.{ + ConfigManager, + PermissionManager, + Resources, + SBuildMessageBuilder +} +import jetbrains.buildServer.web.openapi.{ + PluginDescriptor, + WebControllerManager +} import org.springframework.web.servlet.ModelAndView -import scala.collection.JavaConverters._ - -class BuildSettingEditPage(controllerManager: WebControllerManager, - descriptor: PluginDescriptor, - val permissionManager: PermissionManager, - config: ConfigManager - ) extends SlackController { - controllerManager.registerController(Resources.buildSettingEdit.controllerUrl, this) - - override def handle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView = { +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import scala.jdk.CollectionConverters._ + +class BuildSettingEditPage( + controllerManager: WebControllerManager, + descriptor: PluginDescriptor, + val permissionManager: PermissionManager, + config: ConfigManager +) extends SlackController { + controllerManager.registerController( + Resources.buildSettingEdit.controllerUrl, + this + ) + + override def handle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView = { import com.fpd.teamcity.slack.Helpers.Implicits._ - val view = descriptor.getPluginResourcesPath(Resources.buildSettingEdit.view) + val view = + descriptor.getPluginResourcesPath(Resources.buildSettingEdit.view) val result = for { - key ← request.param("id") - model ← config.buildSetting(key) + key <- request.param("id") + model <- config.buildSetting(key) } yield { - new ModelAndView(view, Map("model" → model, "key" → key).asJava) + new ModelAndView(view, Map("model" -> model, "key" -> key).asJava) } - result getOrElse new ModelAndView(view, Map("defaultMessage" → SBuildMessageBuilder.defaultMessage).asJava) + result getOrElse new ModelAndView( + view, + Map("defaultMessage" -> SBuildMessageBuilder.defaultMessage).asJava + ) } override protected def checkPermission(request: HttpServletRequest): Boolean = - request.param("buildTypeId").exists(id ⇒ permissionManager.buildAccessPermitted(request, id)) + request + .param("buildTypeId") + .exists(id => permissionManager.buildAccessPermitted(request, id)) } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingListPage.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingListPage.scala index 67b03d8..69be07b 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingListPage.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/BuildSettingListPage.scala @@ -1,30 +1,48 @@ package com.fpd.teamcity.slack.pages -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} - import com.fpd.teamcity.slack.Helpers.Implicits._ import com.fpd.teamcity.slack.controllers.SlackController import com.fpd.teamcity.slack.{ConfigManager, PermissionManager, Resources} import jetbrains.buildServer.controllers.BaseController -import jetbrains.buildServer.serverSide.ProjectManager -import jetbrains.buildServer.web.openapi.{PluginDescriptor, WebControllerManager} +import jetbrains.buildServer.web.openapi.{ + PluginDescriptor, + WebControllerManager +} import org.springframework.web.servlet.ModelAndView -import scala.collection.JavaConverters._ +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import scala.jdk.CollectionConverters._ -class BuildSettingListPage(controllerManager: WebControllerManager, - descriptor: PluginDescriptor, - config: ConfigManager, - val permissionManager: PermissionManager, - projectManager: ProjectManager - ) extends BaseController with SlackController { - controllerManager.registerController(Resources.buildSettingList.controllerUrl, this) +class BuildSettingListPage( + controllerManager: WebControllerManager, + descriptor: PluginDescriptor, + config: ConfigManager, + val permissionManager: PermissionManager +) extends BaseController + with SlackController { + controllerManager.registerController( + Resources.buildSettingList.controllerUrl, + this + ) - override def handle(request: HttpServletRequest, response: HttpServletResponse): ModelAndView = { - val view = descriptor.getPluginResourcesPath(Resources.buildSettingList.view) - new ModelAndView(view, Map("list" → config.buildSettingList(request.param("buildTypeId").get).asJava).asJava) + override def handle( + request: HttpServletRequest, + response: HttpServletResponse + ): ModelAndView = { + val view = + descriptor.getPluginResourcesPath(Resources.buildSettingList.view) + new ModelAndView( + view, + Map( + "list" -> config + .buildSettingList(request.param("buildTypeId").get) + .asJava + ).asJava + ) } override protected def checkPermission(request: HttpServletRequest): Boolean = - request.param("buildTypeId").exists(id ⇒ permissionManager.buildAccessPermitted(request, id)) + request + .param("buildTypeId") + .exists(id => permissionManager.buildAccessPermitted(request, id)) } diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/ConfigPage.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/ConfigPage.scala index ea021d4..cb4c602 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/ConfigPage.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/ConfigPage.scala @@ -1,39 +1,54 @@ package com.fpd.teamcity.slack.pages -import java.util - -import javax.servlet.http.HttpServletRequest -import com.fpd.teamcity.slack.{ConfigManager, PermissionManager, Resources, Strings} +import com.fpd.teamcity.slack.{ + ConfigManager, + PermissionManager, + Resources, + Strings +} import jetbrains.buildServer.controllers.admin.AdminPage import jetbrains.buildServer.web.CSRFFilter -import jetbrains.buildServer.web.openapi.{Groupable, PagePlaces, PluginDescriptor} +import jetbrains.buildServer.web.openapi.{ + Groupable, + PagePlaces, + PluginDescriptor +} + +import java.util +import javax.servlet.http.HttpServletRequest class ConfigPage( - extension: ConfigManager, - pagePlaces: PagePlaces, - descriptor: PluginDescriptor, - val permissionManager: PermissionManager - ) - extends - AdminPage( + extension: ConfigManager, + pagePlaces: PagePlaces, + descriptor: PluginDescriptor, + val permissionManager: PermissionManager +) extends AdminPage( pagePlaces, Strings.tabId, descriptor.getPluginResourcesPath(ConfigPage.includeUrl), - Strings.label) + Strings.label + ) with SlackExtension { register() addJsFile(descriptor.getPluginResourcesPath("js/slack-notifier-config.js")) - override def fillModel(model: util.Map[String, AnyRef], request: HttpServletRequest): Unit = { - import collection.JavaConverters._ + override def fillModel( + model: util.Map[String, AnyRef], + request: HttpServletRequest + ): Unit = { import com.fpd.teamcity.slack.Helpers.Implicits._ - model.putAll(extension.details.mapValues(_.getOrElse("")).asJava) + import scala.jdk.CollectionConverters._ + + model.putAll(extension.details.view.mapValues(_.getOrElse("")).toMap.asJava) model.put("error", request.param("error").getOrElse("")) model.put("saveConfigSubmitUrl", Resources.configPage.controllerUrl) - model.put("tcCsrfToken", request.getSession.getAttribute(CSRFFilter.ATTRIBUTE)) + model.put( + "tcCsrfToken", + request.getSession.getAttribute(CSRFFilter.ATTRIBUTE) + ) } override def getGroup: String = Groupable.SERVER_RELATED_GROUP diff --git a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/SlackExtension.scala b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/SlackExtension.scala index d12d8a1..3123e36 100644 --- a/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/SlackExtension.scala +++ b/slackIntegration-server/src/main/scala/com/fpd/teamcity/slack/pages/SlackExtension.scala @@ -8,5 +8,6 @@ import jetbrains.buildServer.web.openapi.SimplePageExtension trait SlackExtension extends SimplePageExtension { protected val permissionManager: PermissionManager - override def isAvailable(request: HttpServletRequest): Boolean = permissionManager.accessPermitted(request) + override def isAvailable(request: HttpServletRequest): Boolean = + permissionManager.accessPermitted(request) } diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/ConfigManagerTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/ConfigManagerTest.scala index 21d8ef0..05dbc97 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/ConfigManagerTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/ConfigManagerTest.scala @@ -2,15 +2,23 @@ package com.fpd.teamcity.slack import com.fpd.teamcity.slack.ConfigManager.BuildSetting import org.scalamock.scalatest.MockFactory -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -class ConfigManagerTest extends FlatSpec with MockFactory with Matchers { +class ConfigManagerTest extends AnyFlatSpec with MockFactory with Matchers { "updateBuildSetting" should "preserve build settings when changing API key" in new CommonMocks { val key = "SomeKey" - manager.update(key, "", personalEnabled = true, enabled = true, "", sendAsAttachment = true) + manager.update( + key, + "", + personalEnabled = true, + enabled = true, + "", + sendAsAttachment = true + ) manager.oauthKey shouldEqual Some(key) - val buildSetting = BuildSetting("", "", "", "{name}", Set.empty) + private val buildSetting = BuildSetting("", "", "", "{name}", Set.empty) manager.updateBuildSetting(buildSetting, None) manager.oauthKey shouldEqual Some(key) manager.allBuildSettingList.values.toSet shouldEqual Set(buildSetting) diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/HelpersTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/HelpersTest.scala index 24cc7fe..18831b5 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/HelpersTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/HelpersTest.scala @@ -5,25 +5,31 @@ import jetbrains.buildServer.messages.Status import jetbrains.buildServer.serverSide._ import org.scalamock.scalatest.MockFactory import org.scalatest.prop.TableDrivenPropertyChecks._ -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ -class HelpersTest extends FlatSpec with MockFactory with Matchers { +class HelpersTest extends AnyFlatSpec with MockFactory with Matchers { "RichBuild.branchMask" should "return correct value" in { - forAll(data) { (branchNameOpt: Option[String], branchMask: String, matches: Boolean) ⇒ - val build = stub[SBuild] - branchNameOpt.foreach { branchName ⇒ - val branch = stub[Branch] - branch.getDisplayName _ when() returns branchName - build.getBranch _ when() returns branch - } - build.matchBranch(branchMask) shouldEqual matches + forAll(data) { + (branchNameOpt: Option[String], branchMask: String, matches: Boolean) => + val build = stub[SBuild] + branchNameOpt.foreach { branchName => + val branch = stub[Branch] + (branch.getDisplayName _).when().returns(branchName) + (build.getBranch _).when().returns(branch) + } + build.matchBranch(branchMask) shouldEqual matches } def data = Table( - ("branchName", "branchMask", "matches"), // First tuple defines column names + ( + "branchName", + "branchMask", + "matches" + ), // First tuple defines column names // Subsequent tuples define the data (Some(""), ".*", true), (Some("default"), "default", true), @@ -35,14 +41,14 @@ class HelpersTest extends FlatSpec with MockFactory with Matchers { "RichBuild.branchMask" should "return correct value for empty branch" in { val build = stub[SBuild] - build.getBranch _ when() returns null + (build.getBranch _).when().returns(null) build.matchBranch(".*") shouldEqual true } "RichBuild.formattedDuration" should "format duration" in { - forAll(data) { (duration: Long, formatted: String) ⇒ + forAll(data) { (duration: Long, formatted: String) => val build = stub[SBuild] - build.getDuration _ when() returns duration + (build.getDuration _).when().returns(duration) build.formattedDuration shouldEqual formatted } @@ -64,70 +70,109 @@ class HelpersTest extends FlatSpec with MockFactory with Matchers { } "RichBuildServer.findPreviousStatus" should "work properly" in { - def stubBranch(branchName: Option[String]) = branchName.map { b ⇒ + def stubBranch(branchName: Option[String]) = branchName.map { b => val branch = stub[Branch] - branch.getDisplayName _ when() returns b + (branch.getDisplayName _).when().returns(b) branch }.orNull - forAll(data) { (previousBuilds: List[PreviousBuild], currentBranch: Option[String], expectedStatus: Status) ⇒ - val buildHistory = stub[BuildHistory] - val buildServer = stub[SBuildServer] - - val currentBuild = stub[SFinishedBuild] - currentBuild.getBranch _ when() returns stubBranch(currentBranch) - - val buildList = previousBuilds.map { previousBuild ⇒ - val build = stub[SFinishedBuild] - build.isPersonal _ when() returns false - build.getBuildStatus _ when() returns previousBuild.status - - val branch = stubBranch(previousBuild.branchName) - build.getBranch _ when() returns branch - build - } - - buildHistory.getEntriesBefore _ when(currentBuild, false) returns buildList.reverse.asJava - buildServer.getHistory _ when() returns buildHistory - - new RichBuildServer(buildServer).findPreviousStatus(currentBuild) shouldEqual expectedStatus + forAll(data) { + ( + previousBuilds: List[PreviousBuild], + currentBranch: Option[String], + expectedStatus: Status + ) => + val buildHistory = stub[BuildHistory] + val buildServer = stub[SBuildServer] + + val currentBuild = stub[SFinishedBuild] + (currentBuild.getBranch _).when().returns(stubBranch(currentBranch)) + + val buildList = previousBuilds.map { previousBuild => + val build = stub[SFinishedBuild] + (build.isPersonal _).when().returns(false) + (build.getBuildStatus _).when().returns(previousBuild.status) + + val branch = stubBranch(previousBuild.branchName) + (build.getBranch _).when().returns(branch) + build + } + + buildHistory.getEntriesBefore _ when (currentBuild, false) returns buildList.reverse.asJava + (buildServer.getHistory _).when().returns(buildHistory) + + new RichBuildServer(buildServer).findPreviousStatus( + currentBuild + ) shouldEqual expectedStatus } case class PreviousBuild(branchName: Option[String], status: Status) def data = Table( - ("previousBuilds", "currentBranch", "expectedStatus"), // First tuple defines column names + ( + "previousBuilds", + "currentBranch", + "expectedStatus" + ), // First tuple defines column names // Subsequent tuples define the data - (List( - PreviousBuild(Some("default"), Status.NORMAL), - PreviousBuild(Some("master"), Status.FAILURE) - ), Some("default"), Status.NORMAL), - (List( - PreviousBuild(Some("default"), Status.NORMAL), - PreviousBuild(Some("default"), Status.FAILURE) - ), Some("default"), Status.FAILURE), - (List( - PreviousBuild(None, Status.NORMAL), - PreviousBuild(Some("default"), Status.FAILURE) - ), None, Status.NORMAL), - (List( - PreviousBuild(None, Status.NORMAL), - PreviousBuild(None, Status.FAILURE) - ), None, Status.FAILURE), - (List( - PreviousBuild(Some("default"), Status.NORMAL), - PreviousBuild(Some("master"), Status.FAILURE) - ), None, Status.NORMAL), - (List( - PreviousBuild(Some("default"), Status.NORMAL), - PreviousBuild(Some("master"), Status.FAILURE) - ), Some("awesome"), Status.NORMAL), + ( + List( + PreviousBuild(Some("default"), Status.NORMAL), + PreviousBuild(Some("master"), Status.FAILURE) + ), + Some("default"), + Status.NORMAL + ), + ( + List( + PreviousBuild(Some("default"), Status.NORMAL), + PreviousBuild(Some("default"), Status.FAILURE) + ), + Some("default"), + Status.FAILURE + ), + ( + List( + PreviousBuild(None, Status.NORMAL), + PreviousBuild(Some("default"), Status.FAILURE) + ), + None, + Status.NORMAL + ), + ( + List( + PreviousBuild(None, Status.NORMAL), + PreviousBuild(None, Status.FAILURE) + ), + None, + Status.FAILURE + ), + ( + List( + PreviousBuild(Some("default"), Status.NORMAL), + PreviousBuild(Some("master"), Status.FAILURE) + ), + None, + Status.NORMAL + ), + ( + List( + PreviousBuild(Some("default"), Status.NORMAL), + PreviousBuild(Some("master"), Status.FAILURE) + ), + Some("awesome"), + Status.NORMAL + ), (Nil, Some("awesome"), Status.NORMAL), - (List( - PreviousBuild(Some("default"), Status.UNKNOWN), - PreviousBuild(Some("default"), Status.NORMAL), - PreviousBuild(Some("master"), Status.FAILURE) - ), Some("awesome"), Status.NORMAL) + ( + List( + PreviousBuild(Some("default"), Status.UNKNOWN), + PreviousBuild(Some("default"), Status.NORMAL), + PreviousBuild(Some("master"), Status.FAILURE) + ), + Some("awesome"), + Status.NORMAL + ) ) } } diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/MessageBuilderTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/MessageBuilderTest.scala index 085a5ec..0d1c1d9 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/MessageBuilderTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/MessageBuilderTest.scala @@ -6,101 +6,136 @@ import com.fpd.teamcity.slack.ConfigManager.BuildSetting import com.fpd.teamcity.slack.SlackGateway.SlackAttachment import jetbrains.buildServer.BuildProblemData import jetbrains.buildServer.messages.Status -import jetbrains.buildServer.serverSide.artifacts.{BuildArtifact, BuildArtifacts, BuildArtifactsViewMode} +import jetbrains.buildServer.serverSide.artifacts.{ + BuildArtifact, + BuildArtifacts, + BuildArtifactsViewMode +} import jetbrains.buildServer.serverSide.{Branch, SBuild, SQueuedBuild} import jetbrains.buildServer.users.SUser import jetbrains.buildServer.vcs.SVcsModification import org.scalamock.scalatest.MockFactory -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ -class SBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { +class SBuildMessageBuilderTest + extends AnyFlatSpec + with MockFactory + with Matchers { import SBuildMessageBuilderTest._ - private val buildSetting = BuildSetting("", "", "art", "", artifactsMask = ".*") + private val buildSetting = + BuildSetting("", "", "art", "", artifactsMask = ".*") "MessageBuilder.compile" should "compile default template" in { implicit val build: SBuild = stub[SBuild] val branch = stub[Branch] - build.getFullName _ when() returns "Full name" - build.getDuration _ when() returns 100 - build.getBuildNumber _ when() returns "2" - build.getBranch _ when() returns branch - branch.getDisplayName _ when() returns "default" - build.getBuildStatus _ when() returns Status.NORMAL + (build.getFullName _).when().returns("Full name") + (build.getDuration _).when().returns(100) + (build.getBuildNumber _).when().returns("2") + (build.getBranch _).when().returns(branch) + (branch.getDisplayName _).when().returns("default") + (build.getBuildStatus _).when().returns(Status.NORMAL) val viewResultsUrl = "http://localhost:8111/viewLog.html?buildId=2" - messageBuilder(viewResultsUrl).compile(SBuildMessageBuilder.defaultMessage, buildSetting) shouldEqual SlackAttachment( + messageBuilder(viewResultsUrl).compile( + SBuildMessageBuilder.defaultMessage, + buildSetting + ) shouldEqual SlackAttachment( s"""<$viewResultsUrl|Full name - 2> |Branch: default |Status: ${Strings.MessageBuilder.statusSucceeded} - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile default template with encoded build name" in { implicit val build: SBuild = stub[SBuild] val branch = stub[Branch] - build.getFullName _ when() returns "Deploy -> test-host.io & demo-host.io <-" - build.getDuration _ when() returns 100 - build.getBuildNumber _ when() returns "2" - build.getBranch _ when() returns branch - branch.getDisplayName _ when() returns "default" - build.getBuildStatus _ when() returns Status.NORMAL + (build.getFullName _) + .when() + .returns("Deploy -> test-host.io & demo-host.io <-") + (build.getDuration _).when().returns(100) + (build.getBuildNumber _).when().returns("2") + (build.getBranch _).when().returns(branch) + (branch.getDisplayName _).when().returns("default") + (build.getBuildStatus _).when().returns(Status.NORMAL) val viewResultsUrl = "http://localhost:8111/viewLog.html?buildId=2" - messageBuilder(viewResultsUrl).compile(SBuildMessageBuilder.defaultMessage, buildSetting) shouldEqual SlackAttachment( + messageBuilder(viewResultsUrl).compile( + SBuildMessageBuilder.defaultMessage, + buildSetting + ) shouldEqual SlackAttachment( s"""<$viewResultsUrl|Deploy -> test-host.io & demo-host.io <- - 2> |Branch: default |Status: ${Strings.MessageBuilder.statusSucceeded} - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile failure template" in { implicit val build: SBuild = stub[SBuild] val branch = stub[Branch] - build.getFullName _ when() returns "Full name" - build.getDuration _ when() returns 100 - build.getBuildNumber _ when() returns "2" - build.getBranch _ when() returns branch - branch.getDisplayName _ when() returns "default" - build.getBuildStatus _ when() returns Status.FAILURE - build.getContainingChanges _ when() returns mockChanges + (build.getFullName _).when().returns("Full name") + (build.getDuration _).when().returns(100) + (build.getBuildNumber _).when().returns("2") + (build.getBranch _).when().returns(branch) + (branch.getDisplayName _).when().returns("default") + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.getContainingChanges _).when().returns(mockChanges) val viewResultsUrl = "http://localhost:8111/viewLog.html?buildId=2" - messageBuilder(viewResultsUrl).compile(SBuildMessageBuilder.defaultMessage, buildSetting) shouldEqual SlackAttachment( + messageBuilder(viewResultsUrl).compile( + SBuildMessageBuilder.defaultMessage, + buildSetting + ) shouldEqual SlackAttachment( s"""<$viewResultsUrl|Full name - 2> |Branch: default |Status: ${Strings.MessageBuilder.statusFailed} |<@nick1> <@nick2> - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template with unknown placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.FAILURE + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.FAILURE) val messageTemplate = """{name} |{unknown} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( """Full name |{unknown} - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template with mentions placeholders and replace it with empty string" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildStatus _ when() returns Status.NORMAL + (build.getFullName _).when().returns("Full name") + (build.getBuildStatus _).when().returns(Status.NORMAL) val messageTemplate = """{name} |{mentions} @@ -113,67 +148,85 @@ class SBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { "MessageBuilder.compile" should "compile template with mentions placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.FAILURE - build.getContainingChanges _ when() returns mockChanges + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.getContainingChanges _).when().returns(mockChanges) val messageTemplate = """{name} |{mentions} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( """Full name |<@nick1> <@nick2> - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template with changes placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.FAILURE - build.getContainingChanges _ when() returns mockChanges + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.getContainingChanges _).when().returns(mockChanges) val messageTemplate = """{name} |{changes} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( """Full name |- Did some changes [name1] |- Did another changes [name2] - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template with reason placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.FAILURE + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.FAILURE) val reasons = List("some reason 1", "some reason 2") - build.getFailureReasons _ when() returns mockReasons(reasons) + (build.getFailureReasons _).when().returns(mockReasons(reasons)) val messageTemplate = """{name} |{reason} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( s"""Full name |Reason: ${reasons.mkString("\n")} - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template without reason placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.NORMAL + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.NORMAL) val reasons = List("some reason 1", "some reason 2") - build.getFailureReasons _ when() returns mockReasons(reasons) + (build.getFailureReasons _).when().returns(mockReasons(reasons)) val messageTemplate = """{name} |{reason} @@ -186,40 +239,53 @@ class SBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { "MessageBuilder.compile" should "compile template with artifacts placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.FAILURE - build.getContainingChanges _ when() returns mockChanges + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.getContainingChanges _).when().returns(mockChanges) val messageTemplate = """{name} |{allArtifactsDownloadUrl} """.stripMargin val downloadUrl = "http://my.teamcity/download/artifacts.zip" - messageBuilder(downloadArtifactsUrl = downloadUrl).compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder(downloadArtifactsUrl = downloadUrl) + .compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( s"""Full name |<$downloadUrl|Download all artifacts> - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template with artifactsRelUrl placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.NORMAL - build.getContainingChanges _ when() returns mockChanges - build.getArtifactsDirectory _ when() returns new File("/full/artifacts/path/my/build/folder/") + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.NORMAL) + (build.getContainingChanges _).when().returns(mockChanges) + (build.getArtifactsDirectory _) + .when() + .returns( + new File( + "/full/artifacts/path/my/build/folder/" + ) + ) val messageTemplate = """{name} |{artifactsRelUrl} """.stripMargin - - messageBuilder(artifactsPath = "/full/artifacts/path/").compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder(artifactsPath = "/full/artifacts/path/") + .compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( s"""Full name |my/build/folder - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile template with parameter placeholders" in { @@ -228,20 +294,23 @@ class SBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { val teamcityParam = "teamcity.param" val teamcityParamValue = "teamcity.param.value" - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.NORMAL - build.getContainingChanges _ when() returns mockChanges + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.NORMAL) + (build.getContainingChanges _).when().returns(mockChanges) val messageTemplate = s"""{name} |{% $teamcityParam%} """.stripMargin - - messageBuilder(params = Map(teamcityParam → teamcityParamValue)).compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder(params = Map(teamcityParam -> teamcityParamValue)) + .compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( s"""Full name |$teamcityParamValue - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile template with parameter placeholder containing emphasis in name" in { @@ -250,84 +319,105 @@ class SBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { val teamcityParam = "teamcity-param" val teamcityParamValue = "teamcity.param.value" - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.NORMAL - build.getContainingChanges _ when() returns mockChanges + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.NORMAL) + (build.getContainingChanges _).when().returns(mockChanges) val messageTemplate = s"""{name} |{%$teamcityParam%} """.stripMargin - - messageBuilder(params = Map(teamcityParam → teamcityParamValue)).compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder(params = Map(teamcityParam -> teamcityParamValue)) + .compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( s"""Full name |$teamcityParamValue - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile template with changes placeholders with non-teamcity committer" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.FAILURE - build.getContainingChanges _ when() returns mockUnknownChange + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.getContainingChanges _).when().returns(mockUnknownChange) val messageTemplate = """{name} |{changes} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( """Full name |- Did some changes [user@unknown.com] - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template for canceled build" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getDuration _ when() returns 0 - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.UNKNOWN + (build.getFullName _).when().returns("Full name") + (build.getDuration _).when().returns(0) + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.UNKNOWN) val messageTemplate = """{name} |{status} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( s"""Full name |${Strings.MessageBuilder.statusCanceled} - """.stripMargin.trim, Status.UNKNOWN.getHtmlColor, "⚪") + """.stripMargin.trim, + Status.UNKNOWN.getHtmlColor, + "⚪" + ) } "MessageBuilder.compile" should "compile template for started build" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getDuration _ when() returns 0 - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.NORMAL + (build.getFullName _).when().returns("Full name") + (build.getDuration _).when().returns(0) + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.NORMAL) val messageTemplate = """{name} |{status} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( s"""Full name |${Strings.MessageBuilder.statusStarted} - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor,"✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile template with artifactLinks placeholder" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getDuration _ when() returns 100 - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.NORMAL - build.getArtifactsDirectory _ when() returns new File("directory") + (build.getFullName _).when().returns("Full name") + (build.getDuration _).when().returns(100) + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.NORMAL) + (build.getArtifactsDirectory _).when().returns(new File("directory")) val artifactsViewer = stub[BuildArtifacts] build.getArtifacts _ when BuildArtifactsViewMode.VIEW_DEFAULT_WITH_ARCHIVES_CONTENT returns artifactsViewer @@ -335,72 +425,103 @@ class SBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { new TestBuildArtifact("artifact.txt", "artifact.txt", true), new TestBuildArtifact("artifact2.txt", "folder/artifact2.txt", true) ) - artifactsViewer.iterateArtifacts _ when * onCall { processor: BuildArtifacts.BuildArtifactsProcessor ⇒ - artifacts.foreach(processor.processBuildArtifact) + artifactsViewer.iterateArtifacts _ when * onCall { + processor: BuildArtifacts.BuildArtifactsProcessor => + artifacts.foreach(processor.processBuildArtifact) } val messageTemplate = """{name} |{artifactLinks} """.stripMargin - messageBuilder().compile(messageTemplate, BuildSetting("", "", "art", "", artifactsMask = ".*")) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + BuildSetting("", "", "art", "", artifactsMask = ".*") + ) shouldEqual SlackAttachment( s"""Full name |${artifactsPublicUrl}directory/artifact.txt |${artifactsPublicUrl}directory/folder/artifact2.txt - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile template for build without branch" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getDuration _ when() returns 100 - build.getBuildNumber _ when() returns "2" - build.getBranch _ when() returns null - build.getBuildStatus _ when() returns Status.NORMAL + (build.getFullName _).when().returns("Full name") + (build.getDuration _).when().returns(100) + (build.getBuildNumber _).when().returns("2") + (build.getBranch _).when().returns(null) + (build.getBuildStatus _).when().returns(Status.NORMAL) val viewResultsUrl = "http://localhost:8111/viewLog.html?buildId=2" - messageBuilder(viewResultsUrl).compile(SBuildMessageBuilder.defaultMessage, buildSetting) shouldEqual SlackAttachment( + messageBuilder(viewResultsUrl).compile( + SBuildMessageBuilder.defaultMessage, + buildSetting + ) shouldEqual SlackAttachment( s"""<$viewResultsUrl|Full name - 2> |Branch: ${Strings.MessageBuilder.unknownBranch} |Status: ${Strings.MessageBuilder.statusSucceeded} - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "✅") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile template with users placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getFullName _ when() returns "Full name" - build.getBuildNumber _ when() returns "2" - build.getBuildStatus _ when() returns Status.FAILURE - build.getContainingChanges _ when() returns mockChanges + (build.getFullName _).when().returns("Full name") + (build.getBuildNumber _).when().returns("2") + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.getContainingChanges _).when().returns(mockChanges) val messageTemplate = """{name} |{users} """.stripMargin - messageBuilder().compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + messageBuilder().compile( + messageTemplate, + buildSetting + ) shouldEqual SlackAttachment( """Full name |name1, name2 - """.stripMargin.trim, Status.FAILURE.getHtmlColor, "⛔") + """.stripMargin.trim, + Status.FAILURE.getHtmlColor, + "⛔" + ) } "MessageBuilder.compile" should "compile template with limited changes placeholders" in { implicit val build: SBuild = stub[SBuild] - build.getBuildStatus _ when() returns Status.NORMAL - build.getContainingChanges _ when() returns mockChanges + (build.getBuildStatus _).when().returns(Status.NORMAL) + (build.getContainingChanges _).when().returns(mockChanges) val messageTemplate = "{changes}" - messageBuilder().compile(messageTemplate, BuildSetting("", "", "", "", maxVcsChanges = 1)) shouldEqual SlackAttachment( - "- Did some changes [name1]", SBuildMessageBuilder.statusNormalColor, "✅") + messageBuilder().compile( + messageTemplate, + BuildSetting("", "", "", "", maxVcsChanges = 1) + ) shouldEqual SlackAttachment( + "- Did some changes [name1]", + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } "MessageBuilder.compile" should "compile template with formattedDuration placeholder" in { implicit val build: SBuild = stub[SBuild] - build.getBuildStatus _ when() returns Status.NORMAL - build.getDuration _ when() returns 10L + (build.getBuildStatus _).when().returns(Status.NORMAL) + (build.getDuration _).when().returns(10L) val messageTemplate = "{formattedDuration}" - messageBuilder().compile(messageTemplate, BuildSetting("", "", "", "", maxVcsChanges = 1)) shouldEqual SlackAttachment( - "10s", SBuildMessageBuilder.statusNormalColor, "✅") + messageBuilder().compile( + messageTemplate, + BuildSetting("", "", "", "", maxVcsChanges = 1) + ) shouldEqual SlackAttachment( + "10s", + SBuildMessageBuilder.statusNormalColor, + "✅" + ) } private def mockChanges = { @@ -408,30 +529,36 @@ class SBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { val vcsModification2 = stub[SVcsModification] val user1 = stub[SUser] val user2 = stub[SUser] - user1.getEmail _ when() returns "nick1" - user1.getDescriptiveName _ when() returns "name1" - user2.getEmail _ when() returns "nick2" - user2.getDescriptiveName _ when() returns "name2" - vcsModification1.getCommitters _ when() returns Set(user1).asJava - vcsModification2.getCommitters _ when() returns Set(user2).asJava - vcsModification1.getDescription _ when() returns "Did some changes\nSecond line" - vcsModification1.getChangeCount _ when() returns 5 - vcsModification2.getDescription _ when() returns "Did another changes\n" - vcsModification2.getChangeCount _ when() returns 1 + (user1.getEmail _).when().returns("nick1") + (user1.getDescriptiveName _).when().returns("name1") + (user2.getEmail _).when().returns("nick2") + (user2.getDescriptiveName _).when().returns("name2") + (vcsModification1.getCommitters _).when().returns(Set(user1).asJava) + (vcsModification2.getCommitters _).when().returns(Set(user2).asJava) + (vcsModification1.getDescription _) + .when() + .returns("Did some changes\nSecond line") + (vcsModification1.getChangeCount _).when().returns(5) + (vcsModification2.getDescription _).when().returns("Did another changes\n") + (vcsModification2.getChangeCount _).when().returns(1) List(vcsModification1, vcsModification2).asJava } private def mockUnknownChange = { val vcsModification = stub[SVcsModification] - vcsModification.getCommitters _ when() returns Set.empty[SUser].asJava - vcsModification.getDescription _ when() returns "Did some changes\n" - vcsModification.getUserName _ when() returns "user@unknown.com" + (vcsModification.getCommitters _).when().returns(Set.empty[SUser].asJava) + (vcsModification.getDescription _).when().returns("Did some changes\n") + (vcsModification.getUserName _).when().returns("user@unknown.com") List(vcsModification).asJava } - private def mockReasons(reasons: List[String]) = reasons.map(reason ⇒ BuildProblemData.createBuildProblem("identity", "custom", reason)).asJava + private def mockReasons(reasons: List[String]) = reasons + .map(reason => + BuildProblemData.createBuildProblem("identity", "custom", reason) + ) + .asJava } object SBuildMessageBuilderTest extends MockFactory { @@ -439,22 +566,35 @@ object SBuildMessageBuilderTest extends MockFactory { private val artifactsPublicUrl = "https://team.city/download/" - def messageBuilder(viewResultsUrl: String = "", downloadArtifactsUrl: String = "", artifactsPath: String = "", params: Map[String, String] = Map.empty)(implicit build: SBuild): SBuildMessageBuilder = { + def messageBuilder( + viewResultsUrl: String = "", + downloadArtifactsUrl: String = "", + artifactsPath: String = "", + params: Map[String, String] = Map.empty + )(implicit build: SBuild): SBuildMessageBuilder = { val context = stub[MessageBuilderContext] - context.getArtifactsPath _ when() returns artifactsPath - context.getViewResultsUrl _ when() returns (_ ⇒ viewResultsUrl) - context.getDownloadAllArtifactsUrl _ when() returns (_ ⇒ downloadArtifactsUrl) - context.userByEmail _ when() returns (x ⇒ Some(x)) - context.getBuildParameter _ when() returns ((_, name) ⇒ params.get(name)) - context.artifactsPublicUrl _ when() returns Some(artifactsPublicUrl) + (() => context.getArtifactsPath).when().returns(artifactsPath) + (() => context.getViewResultsUrl).when().returns(_ => viewResultsUrl) + (() => context.getDownloadAllArtifactsUrl) + .when() + .returns(_ => downloadArtifactsUrl) + (() => context.userByEmail).when().returns(x => Some(x)) + (() => context.getBuildParameter) + .when() + .returns((_, name) => params.get(name)) + (() => context.artifactsPublicUrl).when().returns(Some(artifactsPublicUrl)) new SBuildMessageBuilder(build, context) } - class SQueuedBuildMessageBuilderTest extends FlatSpec with MockFactory with Matchers { + class SQueuedBuildMessageBuilderTest + extends AnyFlatSpec + with MockFactory + with Matchers { import SQueuedBuildMessageBuilderTest._ - private val buildSetting = BuildSetting("", "", "art", "", artifactsMask = ".*") + private val buildSetting = + BuildSetting("", "", "art", "", artifactsMask = ".*") "SQueuedBuildMessageBuilder.compile" should "compile template with name branch status and link placeholder" in { implicit val build: SQueuedBuild = stub[SQueuedBuild] @@ -465,30 +605,44 @@ object SBuildMessageBuilderTest extends MockFactory { |{link} """.stripMargin - queuedMessageBuilder("Full name", "Branch name", "/some/build/url").compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( + queuedMessageBuilder("Full name", "Branch name", "/some/build/url") + .compile(messageTemplate, buildSetting) shouldEqual SlackAttachment( s"""Full name |Branch name |${Strings.MessageBuilder.statusQueued} |/some/build/url - """.stripMargin.trim, SBuildMessageBuilder.statusNormalColor, "⚪") + """.stripMargin.trim, + SBuildMessageBuilder.statusNormalColor, + "⚪" + ) } } object SQueuedBuildMessageBuilderTest extends MockFactory { - def queuedMessageBuilder(queuedBuildName: String, queuedBuildBranch: String ,queuedBuildUrl: String, params: Map[String, String] = Map.empty)(implicit build: SQueuedBuild): SQueuedBuildMessageBuilder = { + def queuedMessageBuilder( + queuedBuildName: String, + queuedBuildBranch: String, + queuedBuildUrl: String, + params: Map[String, String] = Map.empty + )(implicit build: SQueuedBuild): SQueuedBuildMessageBuilder = { val context = stub[MessageBuilderContext] - context.getQueuedBuildName _ when() returns (_ => queuedBuildName) - context.getQueuedBuildBranch _ when() returns (_ => queuedBuildBranch) - context.getQueuedBuildUrl _ when() returns (_ => queuedBuildUrl) - context.getQueuedBuildParameter _ when() returns ((_, name) ⇒ params.get(name)) + (() => context.getQueuedBuildName).when().returns(_ => queuedBuildName) + (() => context.getQueuedBuildBranch) + .when() + .returns(_ => queuedBuildBranch) + (() => context.getQueuedBuildUrl).when().returns(_ => queuedBuildUrl) + (() => context.getQueuedBuildParameter) + .when() + .returns((_, name) => params.get(name)) new SQueuedBuildMessageBuilder(build, context) } } - class TestBuildArtifact(name: String, relativePath: String, file: Boolean) extends BuildArtifact { + class TestBuildArtifact(name: String, relativePath: String, file: Boolean) + extends BuildArtifact { override def isArchive = false override def isFile = file diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderAsyncTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderAsyncTest.scala index 65017fb..4f5b2af 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderAsyncTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderAsyncTest.scala @@ -1,58 +1,64 @@ package com.fpd.teamcity.slack import com.fpd.teamcity.slack.ConfigManager.BuildSettingFlag -import com.fpd.teamcity.slack.SlackGateway.{MessageSent, SlackChannel, SlackUser} -import com.ullink.slack.simpleslackapi.SlackMessageHandle -import com.ullink.slack.simpleslackapi.replies.SlackMessageReply +import com.fpd.teamcity.slack.SlackGateway.{SlackChannel, SlackUser} import jetbrains.buildServer.messages.Status import jetbrains.buildServer.users.SUser import org.scalamock.scalatest.AsyncMockFactory -import org.scalatest.{AsyncFlatSpec, Matchers} +import org.scalatest.flatspec.AsyncFlatSpec +import org.scalatest.matchers.should.Matchers import scala.concurrent.Future -import scala.util.Success import NotificationSenderTest._ -class NotificationSenderAsyncTest extends AsyncFlatSpec with AsyncMockFactory with Matchers { +class NotificationSenderAsyncTest + extends AsyncFlatSpec + with AsyncMockFactory + with Matchers { "NotificationSender.send" should "send message to channel for non-personal build" in { - val sent = successfulSent - val context = new Context { + var result: Future[Vector[Unit]] = Future.successful(Vector()) + + new Context { def settingFlags = Set(BuildSettingFlag.failure) - build.getBuildStatus _ when() returns Status.FAILURE - build.isPersonal _ when() returns false - gateway.sendMessage _ when(SlackChannel(channelName), *) returns Future.successful(sent) + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.isPersonal _).when().returns(false) + gateway.sendMessage _ when (SlackChannel( + channelName + ), *) returns Future.unit - val result = sender.send(build, Set(BuildSettingFlag.failure)) + result = sender.send(build, Set(BuildSettingFlag.failure)) } - context.result.map(_.head shouldEqual sent) + result.map(_.size shouldEqual 1) } "NotificationSender.send" should "send private message to build's owner for personal build" in { - val sent = successfulSent + var result: Future[Vector[Unit]] = Future.successful(Vector()) - val context = new Context { + new Context { def settingFlags = Set(BuildSettingFlag.failure) - build.getBuildStatus _ when() returns Status.FAILURE - build.isPersonal _ when() returns true + (build.getBuildStatus _).when().returns(Status.FAILURE) + (build.isPersonal _).when().returns(true) val email = "some@email.com" - val user = stub[SUser] - user.getEmail _ when() returns email - build.getOwner _ when() returns user + private val user = stub[SUser] + (user.getEmail _).when().returns(email) + (build.getOwner _).when().returns(user) - gateway.sendMessage _ when(SlackUser(email), *) returns Future.successful(sent) + (gateway.sendMessage _) + .when(SlackUser(email), *) + .returns( + Future.unit + ) - val result = sender.send(build, Set(BuildSettingFlag.failure)) + result = sender.send(build, Set(BuildSettingFlag.failure)) } - context.result.map(_.head shouldEqual sent) + result.map(_.size shouldEqual 1) } - - def successfulSent: MessageSent = Success(stub[SlackMessageHandle[SlackMessageReply]]) } diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderTest.scala index d1db693..bb5b5cd 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/NotificationSenderTest.scala @@ -1,47 +1,65 @@ package com.fpd.teamcity.slack import com.fpd.teamcity.slack.ConfigManager.BuildSettingFlag.BuildSettingFlag -import com.fpd.teamcity.slack.ConfigManager.{BuildSetting, BuildSettingFlag, Config} +import com.fpd.teamcity.slack.ConfigManager.{ + BuildSetting, + BuildSettingFlag, + Config +} import com.fpd.teamcity.slack.SlackGateway.SlackAttachment import jetbrains.buildServer.messages.Status import jetbrains.buildServer.serverSide.{Branch, SBuild} import jetbrains.buildServer.vcs.SVcsModification import org.scalamock.scalatest.MockFactory -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -import scala.collection.JavaConverters._ +import scala.jdk.CollectionConverters._ -class NotificationSenderTest extends FlatSpec with MockFactory with Matchers { +class NotificationSenderTest + extends AnyFlatSpec + with MockFactory + with Matchers { import NotificationSenderTest._ "NotificationSender.prepareSettings" should "return setting if build success" in new Context { def settingFlags = Set(BuildSettingFlag.success) - sender.prepareSettings(build, Set(BuildSettingFlag.success)).toSet shouldEqual Set(setting) + sender + .prepareSettings(build, Set(BuildSettingFlag.success)) + .toSet shouldEqual Set(setting) } "NotificationSender.prepareSettings" should "not return setting if build success" in new Context { - def settingFlags = Set(BuildSettingFlag.success, BuildSettingFlag.failureToSuccess) + def settingFlags = + Set(BuildSettingFlag.success, BuildSettingFlag.failureToSuccess) - sender.prepareSettings(build, Set(BuildSettingFlag.success)).toSet shouldEqual Set.empty[BuildSetting] + sender + .prepareSettings(build, Set(BuildSettingFlag.success)) + .toSet shouldEqual Set.empty[BuildSetting] } "NotificationSender.prepareSettings" should "not return setting if build fails" in new Context { - def settingFlags = Set(BuildSettingFlag.failure, BuildSettingFlag.successToFailure) + def settingFlags = + Set(BuildSettingFlag.failure, BuildSettingFlag.successToFailure) - sender.prepareSettings(build, Set(BuildSettingFlag.failure)).toSet shouldEqual Set.empty[BuildSetting] + sender + .prepareSettings(build, Set(BuildSettingFlag.failure)) + .toSet shouldEqual Set.empty[BuildSetting] } "NotificationSender.prepareSettings" should "return setting if build fails" in new Context { def settingFlags = Set(BuildSettingFlag.failure) - sender.prepareSettings(build, Set(BuildSettingFlag.failure)).toSet shouldEqual Set(setting) + sender + .prepareSettings(build, Set(BuildSettingFlag.failure)) + .toSet shouldEqual Set(setting) } "NotificationSender.shouldSendPersonal" should "return true" in new Context { def settingFlags = Set(BuildSettingFlag.failure) manager.setConfig(manager.config.get.copy(personalEnabled = Some(true))) - build.getBuildStatus _ when() returns Status.FAILURE + (build.getBuildStatus _).when().returns(Status.FAILURE) sender.shouldSendPersonal(build) shouldEqual true } @@ -49,14 +67,14 @@ class NotificationSenderTest extends FlatSpec with MockFactory with Matchers { "NotificationSender.shouldSendPersonal" should "return false when personalEnabled is false" in new Context { def settingFlags = Set(BuildSettingFlag.failure) manager.setConfig(manager.config.get.copy(personalEnabled = Some(false))) - build.getBuildStatus _ when() returns Status.FAILURE + (build.getBuildStatus _).when().returns(Status.FAILURE) sender.shouldSendPersonal(build) shouldEqual false } "NotificationSender.shouldSendPersonal" should "return false when build is success" in new Context { def settingFlags = Set(BuildSettingFlag.failure) - build.getBuildStatus _ when() returns Status.NORMAL + (build.getBuildStatus _).when().returns(Status.NORMAL) sender.shouldSendPersonal(build) shouldEqual false } @@ -64,33 +82,42 @@ class NotificationSenderTest extends FlatSpec with MockFactory with Matchers { object NotificationSenderTest { - class NotificationSenderStub(val configManager: ConfigManager, - val gateway: SlackGateway, - val messageBuilderFactory: MessageBuilderFactory - ) extends NotificationSender { - } + class NotificationSenderStub( + val configManager: ConfigManager, + val gateway: SlackGateway, + val messageBuilderFactory: MessageBuilderFactory + ) extends NotificationSender {} trait Context extends CommonMocks { val gateway: SlackGateway = stub[SlackGateway] - val messageBuilderFactory: MessageBuilderFactory = stub[MessageBuilderFactory] + val messageBuilderFactory: MessageBuilderFactory = + stub[MessageBuilderFactory] private val builder = stub[SBuildMessageBuilder] - builder.compile _ when(*, *) returns SlackAttachment("", "", "") + builder.compile _ when (*, *) returns SlackAttachment("", "", "") (messageBuilderFactory.createForBuild(_: SBuild)) when * returns builder - val sender = new NotificationSenderStub(manager, gateway, messageBuilderFactory) + val sender = + new NotificationSenderStub(manager, gateway, messageBuilderFactory) def settingFlags: Set[BuildSettingFlag] val channelName = "general" - val setting = BuildSetting("buildTypeId", "my-branch", channelName, "", settingFlags) + val setting = + BuildSetting("buildTypeId", "my-branch", channelName, "", settingFlags) val build: SBuild = stub[SBuild] val branch: Branch = stub[Branch] - manager.setConfig(Config("", Map("some-key" → setting))) - - branch.getDisplayName _ when() returns setting.branchMask - build.getBuildTypeId _ when() returns setting.buildTypeId - build.getBranch _ when() returns branch - build.getContainingChanges _ when() returns List.empty[SVcsModification].asJava + manager.setConfig(Config("", Map("some-key" -> setting))) + + (branch.getDisplayName _).when().returns(setting.branchMask) + (build.getBuildTypeId _).when().returns(setting.buildTypeId) + (build.getBranch _).when().returns(branch) + (build.getContainingChanges _) + .when() + .returns( + List + .empty[SVcsModification] + .asJava + ) } } diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/SlackServerAdapterTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/SlackServerAdapterTest.scala index 142bf7f..0713478 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/SlackServerAdapterTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/SlackServerAdapterTest.scala @@ -5,12 +5,16 @@ import com.fpd.teamcity.slack.ConfigManager.BuildSettingFlag.BuildSettingFlag import com.fpd.teamcity.slack.SlackServerAdapter._ import jetbrains.buildServer.messages.Status import org.scalamock.scalatest.MockFactory +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks._ -import org.scalatest.{FlatSpec, Matchers} -class SlackServerAdapterTest extends FlatSpec with MockFactory with Matchers { +class SlackServerAdapterTest + extends AnyFlatSpec + with MockFactory + with Matchers { "SlackServerAdapter.statusChanged" should "work properly" in { - forAll(data) { (previous: Status, current: Status, changed: Boolean) ⇒ + forAll(data) { (previous: Status, current: Status, changed: Boolean) => statusChanged(previous, current) shouldEqual changed } @@ -34,8 +38,9 @@ class SlackServerAdapterTest extends FlatSpec with MockFactory with Matchers { } "SlackServerAdapter.calculateFlags" should "work properly" in { - forAll(data) { (previous: Status, current: Status, flags: Set[BuildSettingFlag]) ⇒ - calculateFlags(previous, current) shouldEqual flags + forAll(data) { + (previous: Status, current: Status, flags: Set[BuildSettingFlag]) => + calculateFlags(previous, current) shouldEqual flags } import BuildSettingFlag._ diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSaveTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSaveTest.scala index d8857bd..2c1e997 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSaveTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsSaveTest.scala @@ -5,15 +5,16 @@ import javax.servlet.http.HttpServletRequest import com.fpd.teamcity.slack.ConfigManager.Config import com.fpd.teamcity.slack.Strings.BuildSettingsController._ import com.fpd.teamcity.slack.{CommonMocks, PermissionManager, SlackGateway} -import com.ullink.slack.simpleslackapi.{SlackChannel, SlackSession} -import jetbrains.buildServer.web.openapi.{PluginDescriptor, WebControllerManager} +import jetbrains.buildServer.web.openapi.{ + PluginDescriptor, + WebControllerManager +} import org.scalatest.prop.TableDrivenPropertyChecks._ import org.scalamock.scalatest.MockFactory -import org.scalatest.{FlatSpec, Matchers} - -import scala.util.{Failure, Success} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -class BuildSettingsSaveTest extends FlatSpec with Matchers { +class BuildSettingsSaveTest extends AnyFlatSpec with Matchers { import BuildSettingsSaveTest._ @@ -21,8 +22,9 @@ class BuildSettingsSaveTest extends FlatSpec with Matchers { val requestParams = correctRequestParams val request = stubRequest(requestParams) - session.findChannelByName _ when requestParams("slackChannel") returns stub[SlackChannel] - gateway.sessionByConfig _ when * returns Success(session) + (gateway.isChannelExists _) + .when(requestParams("slackChannel")) + .returns(Some(true)) buildSettingsSave.handleSave(request) shouldEqual "" @@ -38,14 +40,11 @@ class BuildSettingsSaveTest extends FlatSpec with Matchers { "BuildSettingsSave.handleSave" should "save empty channel name and checked notify committers flag" in new Context { val requestParams = correctRequestParams ++ Map( - "slackChannel" → "", - "notifyCommitter" → "1" + "slackChannel" -> "", + "notifyCommitter" -> "1" ) val request = stubRequest(requestParams) - session.findChannelByName _ when requestParams("slackChannel") returns stub[SlackChannel] - gateway.sessionByConfig _ when * returns Success(session) - buildSettingsSave.handleSave(request) shouldEqual "" val settingList = manager.allBuildSettingList @@ -60,86 +59,97 @@ class BuildSettingsSaveTest extends FlatSpec with Matchers { } "BuildSettingsSave.handleSave" should "fail in case of missed required params" in new Context { - forAll(data) { (requestParams: Map[String, String]) ⇒ - buildSettingsSave.handleSave(stubRequest(requestParams)) shouldEqual requirementsError + forAll(data) { (requestParams: Map[String, String]) => + buildSettingsSave.handleSave( + stubRequest(requestParams) + ) shouldEqual requirementsError } def data = Table( "requestParams", // First tuple defines column names // Subsequent tuples define the data - Map("branchMask" → ".*"), - Map("buildTypeId" → "MyAwesomeBuildId"), - Map("messageTemplate" → "Build was done") + Map("branchMask" -> ".*"), + Map("buildTypeId" -> "MyAwesomeBuildId"), + Map("messageTemplate" -> "Build was done") ) } "BuildSettingsSave.handleSave" should "fail in case of channel and notify committers are empty" in new Context { - val requestParams = correctRequestParams ++ Map("slackChannel" → "") + val requestParams = correctRequestParams ++ Map("slackChannel" -> "") val request = stubRequest(requestParams) - buildSettingsSave.handleSave(request) shouldEqual channelOrNotifyCommitterError + buildSettingsSave.handleSave( + request + ) shouldEqual channelOrNotifyCommitterError } "BuildSettingsSave.handleSave" should "fail in case of broken branch mask regular expression" in new Context { - val requestParams = correctRequestParams ++ Map("branchMask" → ".{1,0}") + val requestParams = correctRequestParams ++ Map("branchMask" -> ".{1,0}") val request = stubRequest(requestParams) - session.findChannelByName _ when requestParams("slackChannel") returns stub[SlackChannel] - gateway.sessionByConfig _ when * returns Success(session) + (gateway.isChannelExists _) + .when(requestParams("slackChannel")) + .returns(Some(true)) buildSettingsSave.handleSave(request) shouldEqual compileBranchMaskError } "BuildSettingsSave.handleSave" should "fail in case of broken artifacts mask regular expression" in new Context { - val requestParams = correctRequestParams ++ Map("artifactsMask" → ".{1,0}") + val requestParams = correctRequestParams ++ Map("artifactsMask" -> ".{1,0}") val request = stubRequest(requestParams) - session.findChannelByName _ when requestParams("slackChannel") returns stub[SlackChannel] - gateway.sessionByConfig _ when * returns Success(session) + (gateway.isChannelExists _) + .when(requestParams("slackChannel")) + .returns(Some(true)) buildSettingsSave.handleSave(request) shouldEqual compileArtifactsMaskError } "BuildSettingsSave.handleSave" should "fail in case of unknown channel" in new Context { val request = stubRequest(correctRequestParams) - gateway.sessionByConfig _ when * returns Success(session) - - buildSettingsSave.handleSave(request) shouldEqual s"Unable to find channel with name ${correctRequestParams("slackChannel")}" - } - "BuildSettingsSave.handleSave" should "fail in case of failed session creation" in new Context { - val request = stubRequest(correctRequestParams) - val exception = new Exception("error") - gateway.sessionByConfig _ when * returns Failure(new Exception("error")) + (gateway.isChannelExists _) + .when(correctRequestParams("slackChannel")) + .returns(Some(false)) - buildSettingsSave.handleSave(request) shouldEqual sessionByConfigError(exception.getMessage) + buildSettingsSave.handleSave( + request + ) shouldEqual s"Unable to find channel with name ${correctRequestParams("slackChannel")}" } } object BuildSettingsSaveTest extends MockFactory { val correctRequestParams = Map( - "branchMask" → ".*", - "slackChannel" → "someChannel", - "buildTypeId" → "MyAwesomeBuildId", - "messageTemplate" → "Build was done" + "branchMask" -> ".*", + "slackChannel" -> "someChannel", + "buildTypeId" -> "MyAwesomeBuildId", + "messageTemplate" -> "Build was done" ) private trait Context extends CommonMocks { val controllerManager: WebControllerManager = stub[WebControllerManager] val gateway: SlackGateway = stub[SlackGateway] val permissionManager: PermissionManager = stub[PermissionManager] - implicit private val pluginDescriptor: PluginDescriptor = stub[PluginDescriptor] - val session = stub[SlackSession] + implicit private val pluginDescriptor: PluginDescriptor = + stub[PluginDescriptor] manager.setConfig(Config("")) - val buildSettingsSave = new BuildSettingsSave(manager, controllerManager, gateway, permissionManager, pluginDescriptor) + val buildSettingsSave = new BuildSettingsSave( + manager, + controllerManager, + gateway, + permissionManager, + pluginDescriptor + ) } def stubRequest(params: Map[String, String]) = { val request = stub[HttpServletRequest] - request.getParameter _ when * onCall { key: String ⇒ params.get(key).orNull } + request.getParameter _ when * onCall { key: String => + params.get(key).orNull + } request } } diff --git a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTryTest.scala b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTryTest.scala index 174c82b..b1c4793 100644 --- a/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTryTest.scala +++ b/slackIntegration-server/src/test/scala/com/fpd/teamcity/slack/controllers/BuildSettingsTryTest.scala @@ -1,51 +1,61 @@ package com.fpd.teamcity.slack.controllers import com.fpd.teamcity.slack.ConfigManager.BuildSetting -import com.fpd.teamcity.slack.SlackGateway.{Destination, SlackChannel, SlackUser} +import com.fpd.teamcity.slack.SlackGateway.{ + Destination, + SlackChannel, + SlackUser +} import jetbrains.buildServer.serverSide.{Branch, SFinishedBuild} import jetbrains.buildServer.users.SUser import org.scalamock.scalatest.MockFactory +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks._ -import org.scalatest.{FlatSpec, Matchers} -class BuildSettingsTryTest extends FlatSpec with MockFactory with Matchers { +class BuildSettingsTryTest extends AnyFlatSpec with MockFactory with Matchers { "BuildSettingsTry.findPreviousBuild" should "work" in { val buildTypeId = "MyBuildTypeId" // Branches val branchMaster = stub[Branch] - branchMaster.getDisplayName _ when() returns "master" + (branchMaster.getDisplayName _).when().returns("master") val branchDefault = stub[Branch] - branchDefault.getDisplayName _ when() returns "default" + (branchDefault.getDisplayName _).when().returns("default") // Builds val buildWithoutBranch = stub[SFinishedBuild] - buildWithoutBranch.getBranch _ when() returns null - buildWithoutBranch.getBuildTypeId _ when() returns buildTypeId + (buildWithoutBranch.getBranch _).when().returns(null) + (buildWithoutBranch.getBuildTypeId _).when().returns(buildTypeId) val buildDefault = stub[SFinishedBuild] - buildDefault.getBranch _ when() returns branchDefault - buildDefault.getBuildTypeId _ when() returns buildTypeId + (buildDefault.getBranch _).when().returns(branchDefault) + (buildDefault.getBuildTypeId _).when().returns(buildTypeId) val buildMaster = stub[SFinishedBuild] - buildMaster.getBranch _ when() returns branchMaster - buildMaster.getBuildTypeId _ when() returns buildTypeId + (buildMaster.getBranch _).when().returns(branchMaster) + (buildMaster.getBuildTypeId _).when().returns(buildTypeId) val buildPersonal = stub[SFinishedBuild] - buildPersonal.isPersonal _ when() returns true + (buildPersonal.isPersonal _).when().returns(true) val buildWithPrevious = stub[SFinishedBuild] - buildWithPrevious.isPersonal _ when() returns true - buildWithPrevious.getPreviousFinished _ when() returns buildMaster + (buildWithPrevious.isPersonal _).when().returns(true) + (buildWithPrevious.getPreviousFinished _).when().returns(buildMaster) // settings val settingMatchAll = BuildSetting(buildTypeId, ".*", "", "") val settingMatchDefault = BuildSetting(buildTypeId, "default", "", "") // Assertion - forAll(data) { (build: SFinishedBuild, buildSetting: BuildSetting, found: Option[SFinishedBuild]) ⇒ - BuildSettingsTry.filterMatchBuild(buildSetting)(build) shouldEqual found + forAll(data) { + ( + build: SFinishedBuild, + buildSetting: BuildSetting, + found: Option[SFinishedBuild] + ) => + BuildSettingsTry.filterMatchBuild(buildSetting)(build) shouldEqual found } def data = @@ -61,8 +71,9 @@ class BuildSettingsTryTest extends FlatSpec with MockFactory with Matchers { } "BuildSettingsTry.detectDestination" should "work" in { - forAll(data) { (setting: BuildSetting, user: SUser, expected: Option[Destination]) ⇒ - BuildSettingsTry.detectDestination(setting, user) shouldEqual expected + forAll(data) { + (setting: BuildSetting, user: SUser, expected: Option[Destination]) => + BuildSettingsTry.detectDestination(setting, user) shouldEqual expected } def data = { @@ -72,14 +83,22 @@ class BuildSettingsTryTest extends FlatSpec with MockFactory with Matchers { val branchName = "default" val user = stub[SUser] - user.getEmail _ when() returns email + (user.getEmail _).when().returns(email) Table( ("setting", "user", "expected"), // First tuple defines column names // Subsequent tuples define the data (BuildSetting(buildTypeId, branchName, "", ""), user, None), - (BuildSetting(buildTypeId, branchName, channelName, ""), user, Some(SlackChannel(channelName))), - (BuildSetting(buildTypeId, branchName, "", "", notifyCommitter = true), user, Some(SlackUser(email))) + ( + BuildSetting(buildTypeId, branchName, channelName, ""), + user, + Some(SlackChannel(channelName)) + ), + ( + BuildSetting(buildTypeId, branchName, "", "", notifyCommitter = true), + user, + Some(SlackUser(email)) + ) ) } }