diff --git a/CHANGELOG.md b/CHANGELOG.md index d33dfda..d190088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### 🤖 Application - Allow configuring listening port +- Allow changing naming pattern for Pull Requests ### 🐳 Docker image @@ -13,6 +14,7 @@ ### ☸️ Helm Chart - Add `.Values.app.listeningPort` parameter +- Add `.Values.app.configuration.namingPattern` parameters ## v0.2.0 diff --git a/config/config.example.yaml b/config/config.example.yaml index cd48617..dba5457 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -57,3 +57,17 @@ projects: gitea: owner: justusbunsi name: example-repo + +# Define pull request names from SonarScanner analysis. Default pattern matches the Jenkins Gitea plugin schema. +namingPattern: + # Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. + # That integer part is identical to the pull request ID in Gitea. + regex: "^PR-(\\d+)$" + + # Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. + # See: https://pkg.go.dev/fmt#hdr-Printing + template: "PR-%d" + + # Example for integer-only names + # # regex: "^(\\d+)$" + # # template: "%d" diff --git a/helm/README.md b/helm/README.md index a7cb6cf..8e86536 100644 --- a/helm/README.md +++ b/helm/README.md @@ -53,20 +53,22 @@ You can find a full changelog in the [main repository](https://codeberg.org/just ### App parameters -| Name | Description | Value | -| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | -| `app.listeningPort` | Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. | `3000` | -| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | -| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | -| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | -| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | -| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | -| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | -| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | -| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | -| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| Name | Description | Value | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| `app.configLocationOverride` | Override the default location of the configuration file (`/home/bot/config/config.yaml`). **Available since Chart version `0.2.0`. Requires at least image tag `v0.2.0`**. (See values file for details) | `""` | +| `app.listeningPort` | Port the application will listening on inside the pod container. **Available since Chart version `0.2.1`. Requires at least image tag `v0.2.1`**. | `3000` | +| `app.configuration.gitea.url` | Endpoint of your Gitea instance. Must be expandable by '/api/v1' to form the API base path as shown in Swagger UI. | `""` | +| `app.configuration.gitea.token.value` | Gitea token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.url` | Endpoint of your SonarQube instance. Must be expandable by '/api' to form the API base path. | `""` | +| `app.configuration.sonarqube.token.value` | SonarQube token as plain text. Can be replaced with `file` key containing path to file. | `""` | +| `app.configuration.sonarqube.additionalMetrics` | Setting this option you can extend that default list by your own metrics. | `[]` | +| `app.configuration.projects[0].sonarqube.key` | Project key inside SonarQube | `""` | +| `app.configuration.projects[0].gitea.owner` | Repository owner inside Gitea | `""` | +| `app.configuration.projects[0].gitea.name` | Repository name inside Gitea | `""` | +| `app.configuration.namingPattern.regex` | Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. | `^PR-(\d+)$` | +| `app.configuration.namingPattern.template` | Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing | `PR-%d` | +| `volumes` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | +| `volumeMounts` | If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly | `[]` | ### Security parameters diff --git a/helm/values.yaml b/helm/values.yaml index d42d99e..7da6a6f 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -129,6 +129,13 @@ app: owner: "" name: "" + # Define pull request names from SonarScanner analysis. Default pattern matches the Jenkins Gitea plugin schema. + # @param app.configuration.namingPattern.regex Regular expression that MUST HAVE exactly ONE GROUP that matches the integer part of the PR. That integer part is identical to the pull request ID in Gitea. + # @param app.configuration.namingPattern.template Valid Go format string. It MUST have one integer placeholder which will be replaced by the pull request ID. See: https://pkg.go.dev/fmt#hdr-Printing + namingPattern: + regex: "^PR-(\\d+)$" + template: "PR-%d" + # @param volumes If token and webhook secrets shall be provided via file, volumes and volume mounts can be configured to setup the environment accordingly volumes: [] # - name: gitea-connection diff --git a/internal/api/gitea_test.go b/internal/api/gitea_test.go index c0083f8..74a407b 100644 --- a/internal/api/gitea_test.go +++ b/internal/api/gitea_test.go @@ -40,6 +40,9 @@ func withValidGiteaSynchronizeRequestData(t *testing.T, jsonBody []byte) (*http. } func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } settings.Gitea = settings.GiteaConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -61,9 +64,16 @@ func TestHandleGiteaCommentWebhookSuccess(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } settings.Gitea = settings.GiteaConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -82,6 +92,10 @@ func TestHandleGiteaCommentWebhookInvalidJSONBody(t *testing.T) { assert.Equal(t, http.StatusUnprocessableEntity, rr.Code) assert.Equal(t, `{"message": "Error parsing POST body."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleGiteaCommentInvalidWebhookSignature(t *testing.T) { diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 10f6326..d4efd3b 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -4,6 +4,7 @@ import ( "bytes" "net/http" "net/http/httptest" + "regexp" "testing" "gitea-sonarqube-pr-bot/internal/settings" @@ -27,6 +28,9 @@ func withValidSonarQubeRequestData(t *testing.T, jsonBody []byte) (*http.Request } func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } settings.SonarQube = settings.SonarQubeConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -44,6 +48,10 @@ func TestHandleSonarQubeWebhookProjectMapped(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleSonarQubeWebhookProjectNotMapped(t *testing.T) { @@ -99,6 +107,9 @@ func TestHandleSonarQubeWebhookInvalidWebhookSignature(t *testing.T) { } func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } settings.SonarQube = settings.SonarQubeConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -117,9 +128,16 @@ func TestHandleSonarQubeWebhookForPullRequest(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Processing data. See bot logs for details."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestHandleSonarQubeWebhookForBranch(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } settings.SonarQube = settings.SonarQubeConfig{ Webhook: &settings.Webhook{ Secret: "", @@ -138,4 +156,8 @@ func TestHandleSonarQubeWebhookForBranch(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, `{"message": "Ignore Hook for non-PR analysis."}`, rr.Body.String()) + + t.Cleanup(func() { + settings.Pattern = nil + }) } diff --git a/internal/clients/sonarqube/main_test.go b/internal/clients/sonarqube/main_test.go new file mode 100644 index 0000000..40fa850 --- /dev/null +++ b/internal/clients/sonarqube/main_test.go @@ -0,0 +1,14 @@ +package sonarqube + +import ( + "io/ioutil" + "log" + "os" + "testing" +) + +// SETUP: mute logs +func TestMain(m *testing.M) { + log.SetOutput(ioutil.Discard) + os.Exit(m.Run()) +} diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index 249349e..aecfb5f 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -7,7 +7,6 @@ import ( "io" "log" "net/http" - "regexp" "strconv" "strings" @@ -16,17 +15,16 @@ import ( ) func ParsePRIndex(name string) (int, error) { - re := regexp.MustCompile(`^PR-(\d+)$`) - res := re.FindSubmatch([]byte(name)) + res := settings.Pattern.RegExp.FindSubmatch([]byte(name)) if len(res) != 2 { - return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", name, re.String()) + return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", name, settings.Pattern.RegExp.String()) } return strconv.Atoi(string(res[1])) } func PRNameFromIndex(index int64) string { - return fmt.Sprintf("PR-%d", index) + return fmt.Sprintf(settings.Pattern.Template, index) } func GetRenderedQualityGate(qg string) string { diff --git a/internal/clients/sonarqube/sonarqube_test.go b/internal/clients/sonarqube/sonarqube_test.go index bb9ef98..d3ef651 100644 --- a/internal/clients/sonarqube/sonarqube_test.go +++ b/internal/clients/sonarqube/sonarqube_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "net/http/httptest" + "regexp" "testing" "gitea-sonarqube-pr-bot/internal/settings" @@ -28,17 +29,41 @@ func (c *ClientMock) Do(req *http.Request) (*http.Response, error) { } func TestParsePRIndexSuccess(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + actual, _ := ParsePRIndex("PR-1337") assert.Equal(t, 1337, actual, "PR index parsing is broken") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestParsePRIndexNonIntegerFailure(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + _, err := ParsePRIndex("PR-invalid") assert.EqualErrorf(t, err, "branch name 'PR-invalid' does not match regex '^PR-(\\d+)$'", "Integer parsing succeeds unexpectedly") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestPRNameFromIndex(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + assert.Equal(t, "PR-1337", PRNameFromIndex(1337)) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestGetRenderedQualityGateSuccess(t *testing.T) { @@ -57,9 +82,16 @@ func TestGetPullRequestUrl(t *testing.T) { sdk := &SonarQubeSdk{ baseUrl: "https://sonarqube.example.com", } + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } actual := sdk.GetPullRequestUrl("test-project", 1337) assert.Equal(t, "https://sonarqube.example.com/dashboard?id=test-project&pullRequest=PR-1337", actual, "PR Dashboard URL building broken") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestRetrieveDataFromApiSuccess(t *testing.T) { @@ -259,6 +291,9 @@ func TestFetchPullRequestsErrorsInResponse(t *testing.T) { } func TestGetPullRequestSuccess(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) @@ -279,6 +314,10 @@ func TestGetPullRequestSuccess(t *testing.T) { assert.Nil(t, err, "Successful data retrieval broken and throws error") assert.IsType(t, &PullRequest{}, actual, "Happy path broken") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestGetPullRequestFetchError(t *testing.T) { @@ -306,6 +345,10 @@ func TestGetPullRequestFetchError(t *testing.T) { } func TestGetPullRequestUnknownPR(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + Template: "PR-%d", + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"pullRequests":[{"key":"PR-1","title":"pr-branch","branch":"pr-branch","base":"main","status":{"qualityGateStatus":"OK","bugs":0,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2022-06-12T11:23:09+0000","target":"main"}]}`)) }) @@ -325,6 +368,10 @@ func TestGetPullRequestUnknownPR(t *testing.T) { _, err := sdk.GetPullRequest("test-project", 1337) assert.Errorf(t, err, "no pull request found with name 'PR-1337'") + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestGetMeasuresSuccess(t *testing.T) { diff --git a/internal/settings/pattern.go b/internal/settings/pattern.go new file mode 100644 index 0000000..32f2e07 --- /dev/null +++ b/internal/settings/pattern.go @@ -0,0 +1,8 @@ +package settings + +import "regexp" + +type PatternConfig struct { + RegExp *regexp.Regexp + Template string +} diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 373ccaf..18a8349 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "regexp" "strings" "github.com/spf13/viper" @@ -11,6 +12,7 @@ var ( Gitea GiteaConfig SonarQube SonarQubeConfig Projects []Project + Pattern *PatternConfig ) func newConfigReader(configFile string) *viper.Viper { @@ -33,6 +35,8 @@ func newConfigReader(configFile string) *viper.Viper { v.SetDefault("sonarqube.webhook.secretFile", "") v.SetDefault("sonarqube.additionalMetrics", []string{}) v.SetDefault("projects", []Project{}) + v.SetDefault("namingPattern.regex", `^PR-(\d+)$`) + v.SetDefault("namingPattern.template", "PR-%d") return v } @@ -71,4 +75,8 @@ func Load(configFile string) { Webhook: NewWebhook(r.GetString, "sonarqube", errCallback), AdditionalMetrics: r.GetStringSlice("sonarqube.additionalMetrics"), } + Pattern = &PatternConfig{ + RegExp: regexp.MustCompile(r.GetString("namingPattern.regex")), + Template: r.GetString("namingPattern.template"), + } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 168c04f..295ed36 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path" + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -29,6 +30,9 @@ projects: gitea: owner: example-organization name: pr-bot +namingPattern: + regex: "^PR-(\\d+)$" + template: "PR-%d" `) func WriteConfigFile(t *testing.T, content []byte) string { @@ -289,3 +293,79 @@ projects: [] assert.Panics(t, func() { Load(c) }, "No panic for empty project mapping that is required") } + +func TestLoadNamingPatternStructure(t *testing.T) { + c := WriteConfigFile(t, defaultConfig) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + Template: "PR-%d", + } + + assert.EqualValues(t, expected, Pattern) +} + +func TestLoadNamingPatternStructureWithInternalDefaults(t *testing.T) { + c := WriteConfigFile(t, []byte( + `gitea: + url: https://example.com/gitea + token: + value: fake-gitea-token +sonarqube: + url: https://example.com/sonarqube + token: + value: fake-sonarqube-token + additionalMetrics: "new_security_hotspots" +projects: + - sonarqube: + key: gitea-sonarqube-pr-bot + gitea: + owner: example-organization + name: pr-bot +`)) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + Template: "PR-%d", + } + + assert.EqualValues(t, expected, Pattern) +} + +func TestLoadNamingPatternStructureInjectedEnvs(t *testing.T) { + os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") + os.Setenv("PRBOT_NAMINGPATTERN_TEMPLATE", "test-%d-pullrequest") + c := WriteConfigFile(t, defaultConfig) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), + Template: "test-%d-pullrequest", + } + + assert.EqualValues(t, expected, Pattern) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") + os.Unsetenv("PRBOT_NAMINGPATTERN_TEMPLATE") + }) +} + +func TestLoadNamingPatternStructureMixedInput(t *testing.T) { + os.Setenv("PRBOT_NAMINGPATTERN_REGEX", "test-(\\d+)-pullrequest") + c := WriteConfigFile(t, defaultConfig) + Load(c) + + expected := &PatternConfig{ + RegExp: regexp.MustCompile(`test-(\d+)-pullrequest`), + Template: "PR-%d", + } + + assert.EqualValues(t, expected, Pattern) + + t.Cleanup(func() { + os.Unsetenv("PRBOT_NAMINGPATTERN_REGEX") + }) +} diff --git a/internal/webhooks/sonarqube/webhook_test.go b/internal/webhooks/sonarqube/webhook_test.go index 941e48f..4c8ac9d 100644 --- a/internal/webhooks/sonarqube/webhook_test.go +++ b/internal/webhooks/sonarqube/webhook_test.go @@ -1,18 +1,29 @@ package sonarqube import ( + "regexp" "testing" + "gitea-sonarqube-pr-bot/internal/settings" + "github.com/stretchr/testify/assert" ) func TestNewWebhook(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "PR-1337", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) response, ok := New(raw) assert.NotNil(t, response) assert.Equal(t, 1337, response.PRIndex) assert.True(t, ok) + + t.Cleanup(func() { + settings.Pattern = nil + }) } func TestNewWebhookInvalidJSON(t *testing.T) { @@ -23,8 +34,16 @@ func TestNewWebhookInvalidJSON(t *testing.T) { } func TestNewWebhookInvalidBranchName(t *testing.T) { + settings.Pattern = &settings.PatternConfig{ + RegExp: regexp.MustCompile(`^PR-(\d+)$`), + } + raw := []byte(`{ "serverUrl": "https://example.com/sonarqube", "taskId": "AXouyxDpizdp4B1K", "status": "SUCCESS", "analysedAt": "2021-05-21T12:12:07+0000", "revision": "f84442009c09b1adc278b6aa80a3853419f54007", "changedAt": "2021-05-21T12:12:07+0000", "project": { "key": "pr-bot", "name": "PR Bot", "url": "https://example.com/sonarqube/dashboard?id=pr-bot" }, "branch": { "name": "invalid", "type": "PULL_REQUEST", "isMain": false, "url": "https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1337" }, "qualityGate": { "name": "PR Bot", "status": "OK", "conditions": [ { "metric": "new_reliability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_maintainability_rating", "operator": "GREATER_THAN", "value": "1", "status": "OK", "errorThreshold": "1" }, { "metric": "new_security_hotspots_reviewed", "operator": "LESS_THAN", "status": "NO_VALUE", "errorThreshold": "100" } ] }, "properties": {} }`) _, ok := New(raw) assert.False(t, ok) + + t.Cleanup(func() { + settings.Pattern = nil + }) }