diff --git a/core/src/main/scala/stryker4s/env/EnvProvider.scala b/core/src/main/scala/stryker4s/env/EnvProvider.scala index e463979b0..82f5e0551 100644 --- a/core/src/main/scala/stryker4s/env/EnvProvider.scala +++ b/core/src/main/scala/stryker4s/env/EnvProvider.scala @@ -1,9 +1,5 @@ package stryker4s.env -trait Environment { - def getEnvVariable(key: String): Option[String] -} - -object SystemEnvironment extends Environment { - override def getEnvVariable(key: String): Option[String] = sys.env.get(key) +object Environment { + type Environment = Map[String, String] } diff --git a/core/src/main/scala/stryker4s/report/DashboardReporter.scala b/core/src/main/scala/stryker4s/report/DashboardReporter.scala index 253c15c51..9c43e5b28 100755 --- a/core/src/main/scala/stryker4s/report/DashboardReporter.scala +++ b/core/src/main/scala/stryker4s/report/DashboardReporter.scala @@ -27,12 +27,13 @@ class DashboardReporter(dashboardConfigProvider: DashboardConfigProvider)( def buildRequest(dashConfig: DashboardConfig, report: MutationTestReport, metrics: MetricsResult) = { import io.circe.{Decoder, Encoder} implicit val decoder: Decoder[DashboardPutResult] = Decoder.forProduct1("href")(DashboardPutResult.apply) - val uri = - uri"${dashConfig.baseUrl}/api/reports/${dashConfig.project}/${dashConfig.version}?module=${dashConfig.module}" + // Separate so any slashes won't be escaped in project or version + val baseUrl = s"${dashConfig.baseUrl}/api/reports/${dashConfig.project}/${dashConfig.version}" + val uri = uri"$baseUrl?module=${dashConfig.module}" val request = basicRequest .header("X-Api-Key", dashConfig.apiKey) - .response(asJson[DashboardPutResult]) .contentType(MediaType.ApplicationJson) + .response(asJson[DashboardPutResult]) .put(uri) dashConfig.reportType match { case Full => @@ -52,7 +53,7 @@ class DashboardReporter(dashboardConfigProvider: DashboardConfigProvider)( response.code match { case StatusCode.Unauthorized => error( - s"Error HTTP PUT $errorBody. Unauthorized. Did you provide the correct api key in the 'STRYKER_DASHBOARD_API_KEY' environment variable?" + s"Error HTTP PUT '$errorBody'. Status code 401 Unauthorized. Did you provide the correct api key in the 'STRYKER_DASHBOARD_API_KEY' environment variable?" ) case statusCode => error( @@ -60,7 +61,7 @@ class DashboardReporter(dashboardConfigProvider: DashboardConfigProvider)( ) } case Left(DeserializationError(original, error)) => - warn(s"Dashboard report was sent successfully, but could not decode the response $original:", error) + warn(s"Dashboard report was sent successfully, but could not decode the response: '$original'. Error:", error) case Right(DashboardPutResult(href)) => info(s"Sent report to dashboard. Available at $href") } diff --git a/core/src/main/scala/stryker4s/report/Reporter.scala b/core/src/main/scala/stryker4s/report/Reporter.scala index 967526f57..4b012d566 100755 --- a/core/src/main/scala/stryker4s/report/Reporter.scala +++ b/core/src/main/scala/stryker4s/report/Reporter.scala @@ -8,7 +8,6 @@ import stryker4s.model.{Mutant, MutantRunResult} import stryker4s.report.dashboard.DashboardConfigProvider import scala.util.{Failure, Try} import sttp.client.HttpURLConnectionBackend -import stryker4s.env.SystemEnvironment class Reporter(implicit config: Config) extends FinishedRunReporter with ProgressReporter with Logging { lazy val reporters: Seq[MutationRunReporter] = config.reporters map { @@ -17,7 +16,7 @@ class Reporter(implicit config: Config) extends FinishedRunReporter with Progres case Json => new JsonReporter(DiskFileIO) case Dashboard => implicit val backend = HttpURLConnectionBackend() - new DashboardReporter(new DashboardConfigProvider(SystemEnvironment)) + new DashboardReporter(new DashboardConfigProvider(sys.env)) } private[this] val progressReporters = reporters collect { case r: ProgressReporter => r } diff --git a/core/src/main/scala/stryker4s/report/dashboard/DashboardConfigProvider.scala b/core/src/main/scala/stryker4s/report/dashboard/DashboardConfigProvider.scala index 85e657311..0dcb525e3 100644 --- a/core/src/main/scala/stryker4s/report/dashboard/DashboardConfigProvider.scala +++ b/core/src/main/scala/stryker4s/report/dashboard/DashboardConfigProvider.scala @@ -2,7 +2,7 @@ package stryker4s.report.dashboard import stryker4s.report.model.DashboardConfig import stryker4s.config.Config import stryker4s.report.dashboard.Providers._ -import stryker4s.env.Environment +import stryker4s.env.Environment.Environment class DashboardConfigProvider(env: Environment)(implicit config: Config) { def resolveConfig(): Either[String, DashboardConfig] = @@ -22,10 +22,11 @@ class DashboardConfigProvider(env: Environment)(implicit config: Config) { module = module ) + private val apiKeyName = "STRYKER_DASHBOARD_API_KEY" private def resolveapiKey() = env - .getEnvVariable("STRYKER_DASHBOARD_API_KEY") - .toRight("STRYKER_DASHBOARD_API_KEY") + .get(apiKeyName) + .toRight(apiKeyName) private def resolveproject() = config.dashboard.project diff --git a/core/src/main/scala/stryker4s/report/dashboard/Providers.scala b/core/src/main/scala/stryker4s/report/dashboard/Providers.scala index faefca60c..ec339fcc9 100755 --- a/core/src/main/scala/stryker4s/report/dashboard/Providers.scala +++ b/core/src/main/scala/stryker4s/report/dashboard/Providers.scala @@ -1,12 +1,12 @@ package stryker4s.report.dashboard import grizzled.slf4j.Logging -import stryker4s.env.Environment +import stryker4s.env.Environment.Environment object Providers extends Logging { def determineCiProvider(env: Environment): Option[CiProvider] = - if (env.getEnvVariable("TRAVIS").isDefined) { + if (env.get("TRAVIS").isDefined) { Some(new TravisProvider(env)) - } else if (env.getEnvVariable("CIRCLECI").isDefined) { + } else if (env.get("CIRCLECI").isDefined) { Some(new CircleProvider(env)) } else { None @@ -18,7 +18,7 @@ object Providers extends Logging { } private def readEnvironmentVariable(name: String, env: Environment): Option[String] = - env.getEnvVariable(name).filter(_.nonEmpty) + env.get(name).filter(_.nonEmpty) class TravisProvider(env: Environment) extends CiProvider { override def determineProject(): Option[String] = diff --git a/core/src/main/scala/stryker4s/run/process/ProcessRunner.scala b/core/src/main/scala/stryker4s/run/process/ProcessRunner.scala index 1db925bc0..121fde7fc 100644 --- a/core/src/main/scala/stryker4s/run/process/ProcessRunner.scala +++ b/core/src/main/scala/stryker4s/run/process/ProcessRunner.scala @@ -29,7 +29,7 @@ trait ProcessRunner extends Logging { } object ProcessRunner { - private val isWindows: Boolean = sys.props("os.name").toLowerCase.contains("windows") + private def isWindows: Boolean = sys.props("os.name").toLowerCase.contains("windows") def apply(): ProcessRunner = { if (isWindows) new WindowsProcessRunner diff --git a/core/src/test/scala/stryker4s/report/DashboardReporterTest.scala b/core/src/test/scala/stryker4s/report/DashboardReporterTest.scala index a2493a62d..b01d15b6c 100755 --- a/core/src/test/scala/stryker4s/report/DashboardReporterTest.scala +++ b/core/src/test/scala/stryker4s/report/DashboardReporterTest.scala @@ -9,37 +9,144 @@ import stryker4s.config.Full import mutationtesting.MutationTestReport import mutationtesting.Metrics import sttp.client._ -import sttp.model.{Header, HeaderNames, MediaType, Method} +import sttp.model.Header +import sttp.model.Method +import stryker4s.report.model.DashboardPutResult +import sttp.model.MediaType +import stryker4s.config.MutationScoreOnly +import sttp.model.StatusCode class DashboardReporterTest extends Stryker4sSuite with MockitoSuite with LogMatchers { describe("buildRequest") { - it("should compose the url") { - implicit val backend = testBackend + it("should compose the request") { + implicit val backend = backendStub val mockDashConfig = mock[DashboardConfigProvider] val sut = new DashboardReporter(mockDashConfig) - val dashConfig = DashboardConfig( - "apiKeyHere", - reportType = Full, - baseUrl = "https://baseurl.com", - project = "project/foo", - version = "version/bar", - module = None - ) + val dashConfig = baseDashConfig val (report, metrics) = baseResults val request = sut.buildRequest(dashConfig, report, metrics) request.uri shouldBe uri"https://baseurl.com/api/reports/project/foo/version/bar" + val jsonBody = { + import mutationtesting.MutationReportEncoder._ + import io.circe.syntax._ + report.asJson.noSpaces + } + request.body shouldBe StringBody(jsonBody, "utf-8", Some(MediaType.ApplicationJson)) request.method shouldBe Method.PUT - request.headers should contain(new Header("X-Api-Key", "apiKeyHere")) - request.headers should contain(new Header(HeaderNames.ContentType, MediaType.ApplicationJson.toString())) + request.headers should contain allOf ( + new Header("X-Api-Key", "apiKeyHere"), + new Header("Content-Type", "application/json") + ) + } + + it("should make a score-only request when score-only is configured") { + implicit val backend = backendStub + val mockDashConfig = mock[DashboardConfigProvider] + val sut = new DashboardReporter(mockDashConfig) + val dashConfig = baseDashConfig.copy(reportType = MutationScoreOnly) + val (report, metrics) = baseResults + + val request = sut.buildRequest(dashConfig, report, metrics) + request.uri shouldBe uri"https://baseurl.com/api/reports/project/foo/version/bar" + val jsonBody = """{"mutationScore":0.0}""" + request.body shouldBe StringBody(jsonBody, "utf-8", Some(MediaType.ApplicationJson)) + } + + it("should add the module if it is present") { + implicit val backend = backendStub + val mockDashConfig = mock[DashboardConfigProvider] + val sut = new DashboardReporter(mockDashConfig) + val dashConfig = baseDashConfig.copy(module = Some("myModule")) + val (report, metrics) = baseResults + + val request = sut.buildRequest(dashConfig, report, metrics) + + request.uri shouldBe uri"https://baseurl.com/api/reports/project/foo/version/bar?module=myModule" } } - def testBackend = SttpBackendStub.synchronous + describe("reportRunFinished") { + it("should send the request") { + implicit val backend = backendStub.whenAnyRequest + .thenRespond(Right(DashboardPutResult("https://hrefHere.com"))) + val mockDashConfig = mock[DashboardConfigProvider] + when(mockDashConfig.resolveConfig()).thenReturn(Right(baseDashConfig)) + val sut = new DashboardReporter(mockDashConfig) + val (report, metrics) = baseResults + + sut.reportRunFinished(report, metrics) + + "Sent report to dashboard. Available at https://hrefHere.com" shouldBe loggedAsInfo + } + + it("log when not being able to resolve dashboard config") { + implicit val backend = backendStub + val mockDashConfig = mock[DashboardConfigProvider] + when(mockDashConfig.resolveConfig()).thenReturn(Left("fooConfigKey")) + val sut = new DashboardReporter(mockDashConfig) + val (report, metrics) = baseResults + + sut.reportRunFinished(report, metrics) + + "Could not resolve dashboard configuration key 'fooConfigKey', not sending report" shouldBe loggedAsWarning + } + + it("should log when a response can't be parsed to a href") { + implicit val backend = backendStub.whenAnyRequest.thenRespond("some other response") + val mockDashConfig = mock[DashboardConfigProvider] + when(mockDashConfig.resolveConfig()).thenReturn(Right(baseDashConfig)) + val sut = new DashboardReporter(mockDashConfig) + val (report, metrics) = baseResults + + sut.reportRunFinished(report, metrics) + + "Dashboard report was sent successfully, but could not decode the response: 'some other response'. Error:" shouldBe loggedAsWarning + } + + it("should log when a 401 is returned by the API") { + implicit val backend = backendStub.whenAnyRequest + .thenRespond(Response(Left(HttpError("auth required")), StatusCode.Unauthorized)) + val mockDashConfig = mock[DashboardConfigProvider] + when(mockDashConfig.resolveConfig()).thenReturn(Right(baseDashConfig)) + val sut = new DashboardReporter(mockDashConfig) + val (report, metrics) = baseResults + + sut.reportRunFinished(report, metrics) + + "Error HTTP PUT 'auth required'. Status code 401 Unauthorized. Did you provide the correct api key in the 'STRYKER_DASHBOARD_API_KEY' environment variable?" shouldBe loggedAsError + } + + it("should log when a error code is returned by the API") { + implicit val backend = + backendStub.whenAnyRequest.thenRespond( + Response(Left(HttpError("internal error")), StatusCode.InternalServerError) + ) + val mockDashConfig = mock[DashboardConfigProvider] + when(mockDashConfig.resolveConfig()).thenReturn(Right(baseDashConfig)) + val sut = new DashboardReporter(mockDashConfig) + val (report, metrics) = baseResults + + sut.reportRunFinished(report, metrics) + + "Failed to PUT report to dashboard. Response status code: 500. Response body: 'internal error'" shouldBe loggedAsError + } + } + + def backendStub = SttpBackendStub.synchronous def baseResults = { val report = MutationTestReport(thresholds = mutationtesting.Thresholds(80, 60), files = Map.empty) val metrics = Metrics.calculateMetrics(report) (report, metrics) } + + def baseDashConfig = DashboardConfig( + apiKey = "apiKeyHere", + reportType = Full, + baseUrl = "https://baseurl.com", + project = "project/foo", + version = "version/bar", + module = None + ) } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ab9ee9430..a9778a264 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { val circe = "0.12.3" val mutationTestingElements = "1.2.1" val mutationTestingMetrics = "1.2.0" - val sttp = "2.0.0-RC2" + val sttp = "2.0.0-RC3" } object test { @@ -32,8 +32,7 @@ object Dependencies { val grizzledSlf4j = "org.clapper" %% "grizzled-slf4j" % versions.grizzledSlf4j val catsCore = "org.typelevel" %% "cats-core" % versions.cats val circeCore = "io.circe" %% "circe-core" % versions.circe - val sttp = "com.softwaremill.sttp.client" %% "core" % versions.sttp - val sttpCirce = "com.softwaremill.sttp.client" %% "circe" % versions.sttp + val sttp = "com.softwaremill.sttp.client" %% "circe" % versions.sttp val mutationTestingElements = "io.stryker-mutator" % "mutation-testing-elements" % versions.mutationTestingElements val mutationTestingMetrics = "io.stryker-mutator" %% "mutation-testing-metrics-circe" % versions.mutationTestingMetrics } diff --git a/project/Settings.scala b/project/Settings.scala index d6397d3e5..5025bd7c6 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -40,7 +40,6 @@ object Settings { Dependencies.log4jslf4jImpl % Test, // Logging tests need a slf4j implementation Dependencies.circeCore, Dependencies.sttp, - Dependencies.sttpCirce, Dependencies.mutationTestingElements, Dependencies.mutationTestingMetrics )