From 0c7c40f43ae6f1e061044f5d8bfaa85b49b12da8 Mon Sep 17 00:00:00 2001 From: Rafael Porres Molina Date: Tue, 15 Mar 2022 13:24:20 +0100 Subject: [PATCH] Add support for Personal Access Token Authentication (#110) * Added tests on config Signed-off-by: Rafa Porres Molina * moved to 1.15 Signed-off-by: Rafa Porres Molina * Upgrade go-jira Signed-off-by: Rafa Porres Molina * Changes to support PAT authentication Signed-off-by: Rafa Porres Molina * addded PAT Signed-off-by: Rafa Porres Molina * Added tests for Personal Access Token config Signed-off-by: Rafa Porres Molina * Removed redundant check from main Signed-off-by: Rafa Porres Molina * Add missing trailing dots in config_test.go Signed-off-by: Rafa Porres Molina * Clarified TODO comment Signed-off-by: Rafa Porres Molina * added trailing dots in config.go Signed-off-by: Rafa Porres Molina * Used anonymous struct and in place test cases Signed-off-by: Rafa Porres Molina * User/Password can be overriden separately Signed-off-by: Rafa Porres Molina --- .github/workflows/build-docker.yaml | 2 +- .github/workflows/test.yaml | 2 +- Dockerfile | 2 +- cmd/jiralert/main.go | 18 +- go.mod | 6 +- go.sum | 20 +- pkg/config/config.go | 62 ++++-- pkg/config/config_test.go | 311 +++++++++++++++++++++++++++- 8 files changed, 381 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml index 752d673..3b06537 100644 --- a/.github/workflows/build-docker.yaml +++ b/.github/workflows/build-docker.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/setup-go@v2 with: - go-version: 1.14.x + go-version: 1.15.x - uses: actions/checkout@v2 - name: Login to quay.io Docker Image Registry uses: docker/login-action@v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d47044d..d7c6b57 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/setup-go@v2 with: - go-version: 1.14.x + go-version: 1.15.x - uses: actions/checkout@v2 - run: make env: diff --git a/Dockerfile b/Dockerfile index c2931bc..c30df20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.14 AS builder +FROM golang:1.15 AS builder WORKDIR /go/src/github.com/prometheus-community/jiralert COPY . /go/src/github.com/prometheus-community/jiralert RUN GO111MODULE=on GOBIN=/tmp/bin make diff --git a/cmd/jiralert/main.go b/cmd/jiralert/main.go index b00c017..4b643a3 100644 --- a/cmd/jiralert/main.go +++ b/cmd/jiralert/main.go @@ -102,11 +102,21 @@ func main() { level.Debug(logger).Log("msg", " matched receiver", "receiver", conf.Name) // TODO: Consider reusing notifiers or just jira clients to reuse connections. - tp := jira.BasicAuthTransport{ - Username: conf.User, - Password: string(conf.Password), + var client *jira.Client + var err error + if conf.User != "" && conf.Password != "" { + tp := jira.BasicAuthTransport{ + Username: conf.User, + Password: string(conf.Password), + } + client, err = jira.NewClient(tp.Client(), conf.APIURL) + } else if conf.PersonalAccessToken != "" { + tp := jira.PATAuthTransport{ + Token: string(conf.PersonalAccessToken), + } + client, err = jira.NewClient(tp.Client(), conf.APIURL) } - client, err := jira.NewClient(tp.Client(), conf.APIURL) + if err != nil { errorHandler(w, http.StatusInternalServerError, err, conf.Name, &data, logger) return diff --git a/go.mod b/go.mod index 02769b5..680ffdb 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,11 @@ module github.com/prometheus-community/jiralert -go 1.14 +go 1.15 require ( - github.com/andygrunwald/go-jira v1.11.2-0.20200514151831-146229d2ab58 - github.com/fatih/structs v1.1.0 // indirect + github.com/andygrunwald/go-jira v1.15.1 github.com/go-kit/kit v0.10.0 github.com/golang/protobuf v1.4.1 // indirect - github.com/google/go-querystring v1.0.0 // indirect github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.6.0 github.com/prometheus/common v0.10.0 // indirect diff --git a/go.sum b/go.sum index b07879f..f721b67 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/andygrunwald/go-jira v1.11.2-0.20200514151831-146229d2ab58 h1:mlwwah02q6TPeO31g6qlmVzNJhhomz0YJcdDNvcJaCA= -github.com/andygrunwald/go-jira v1.11.2-0.20200514151831-146229d2ab58/go.mod h1:jYi4kFDbRPZTJdJOVJO4mpMMIwdB+rcZwSO58DzPd2I= +github.com/andygrunwald/go-jira v1.15.1 h1:6J9aYKb9sW8bxv3pBLYBrs0wdsFrmGI5IeTgWSKWKc8= +github.com/andygrunwald/go-jira v1.15.1/go.mod h1:GIYN1sHOIsENWUZ7B4pDeT/nxEtrZpE8l0987O67ZR8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -54,7 +54,6 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= @@ -76,6 +75,8 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -98,9 +99,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -263,7 +266,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/trivago/tgo v1.0.1/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -284,7 +286,6 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -338,6 +339,9 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/config/config.go b/pkg/config/config.go index 7359bc7..1886de5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -88,21 +88,23 @@ func resolveFilepaths(baseDir string, cfg *Config, logger log.Logger) { cfg.Template = join(cfg.Template) } -// ReceiverConfig is the configuration for one receiver. It has a unique name and includes API access fields (URL, user -// and password) and issue fields (required -- e.g. project, issue type -- and optional -- e.g. priority). +// ReceiverConfig is the configuration for one receiver. It has a unique name and includes API access fields (url and +// auth) and issue fields (required -- e.g. project, issue type -- and optional -- e.g. priority). type ReceiverConfig struct { Name string `yaml:"name" json:"name"` // API access fields - APIURL string `yaml:"api_url" json:"api_url"` - User string `yaml:"user" json:"user"` - Password Secret `yaml:"password" json:"password"` + APIURL string `yaml:"api_url" json:"api_url"` + User string `yaml:"user" json:"user"` + Password Secret `yaml:"password" json:"password"` + PersonalAccessToken Secret `yaml:"personal_access_token" json:"personal_access_token"` // Required issue fields - Project string `yaml:"project" json:"project"` - IssueType string `yaml:"issue_type" json:"issue_type"` - Summary string `yaml:"summary" json:"summary"` - ReopenState string `yaml:"reopen_state" json:"reopen_state"` + Project string `yaml:"project" json:"project"` + IssueType string `yaml:"issue_type" json:"issue_type"` + Summary string `yaml:"summary" json:"summary"` + ReopenState string `yaml:"reopen_state" json:"reopen_state"` + ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"` // Optional issue fields Priority string `yaml:"priority" json:"priority"` @@ -110,7 +112,6 @@ type ReceiverConfig struct { WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"` Fields map[string]interface{} `yaml:"fields" json:"fields"` Components []string `yaml:"components" json:"components"` - ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"` // Label copy settings AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` @@ -158,17 +159,24 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { // We want to set c to the defaults and then overwrite it with the input. // To make unmarshal fill the plain data struct rather than calling UnmarshalYAML // again, we have to hide it using a type indirection. + + // TODO: This function panics when there are no defaults. This needs to be fixed. + type plain Config if err := unmarshal((*plain)(c)); err != nil { return err } + if (c.Defaults.User != "" || c.Defaults.Password != "") && c.Defaults.PersonalAccessToken != "" { + return fmt.Errorf("bad auth config in defaults section: user/password and PAT authentication are mutually exclusive") + } + for _, rc := range c.Receivers { if rc.Name == "" { return fmt.Errorf("missing name for receiver %+v", rc) } - // Check API access fields + // Check API access fields. if rc.APIURL == "" { if c.Defaults.APIURL == "" { return fmt.Errorf("missing api_url in receiver %q", rc.Name) @@ -178,20 +186,30 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if _, err := url.Parse(rc.APIURL); err != nil { return fmt.Errorf("invalid api_url %q in receiver %q: %s", rc.APIURL, rc.Name, err) } - if rc.User == "" { - if c.Defaults.User == "" { - return fmt.Errorf("missing user in receiver %q", rc.Name) - } - rc.User = c.Defaults.User + + if (rc.User != "" || rc.Password != "") && rc.PersonalAccessToken != "" { + return fmt.Errorf("bad auth config in receiver %q: user/password and PAT authentication are mutually exclusive", rc.Name) } - if rc.Password == "" { - if c.Defaults.Password == "" { - return fmt.Errorf("missing password in receiver %q", rc.Name) + + if (rc.User == "" || rc.Password == "") && rc.PersonalAccessToken == "" { + if rc.User == "" && c.Defaults.User != "" { + rc.User = c.Defaults.User + } + + if rc.Password == "" && c.Defaults.Password != "" { + rc.Password = c.Defaults.Password + } + + if rc.User != "" && rc.Password != "" { + // Nothing to do, we're ready to go with basic auth. + } else if c.Defaults.PersonalAccessToken != "" { + rc.PersonalAccessToken = c.Defaults.PersonalAccessToken + } else { + return fmt.Errorf("missing authentication in receiver %q", rc.Name) } - rc.Password = c.Defaults.Password } - // Check required issue fields + // Check required issue fields. if rc.Project == "" { if c.Defaults.Project == "" { return fmt.Errorf("missing project in receiver %q", rc.Name) @@ -223,7 +241,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { rc.ReopenDuration = c.Defaults.ReopenDuration } - // Populate optional issue fields, where necessary + // Populate optional issue fields, where necessary. if rc.Priority == "" && c.Defaults.Priority != "" { rc.Priority = c.Defaults.Priority } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f6d741e..4537fb5 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -16,10 +16,12 @@ import ( "io/ioutil" "os" "path" + "reflect" "testing" "github.com/go-kit/kit/log" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" ) const testConf = ` @@ -76,6 +78,7 @@ receivers: template: jiralert.tmpl ` +// Generic test that loads the testConf with no errors. func TestLoadFile(t *testing.T) { dir, err := ioutil.TempDir("", "test_jiralert") require.NoError(t, err) @@ -88,5 +91,311 @@ func TestLoadFile(t *testing.T) { require.NoError(t, err) require.Equal(t, testConf, string(content)) - // TODO(bwplotka): Add proper test cases on config struct. +} + +// A test version of the ReceiverConfig struct to create test yaml fixtures. +type receiverTestConfig struct { + Name string `yaml:"name,omitempty"` + APIURL string `yaml:"api_url,omitempty"` + User string `yaml:"user,omitempty"` + Password string `yaml:"password,omitempty"` + PersonalAccessToken string `yaml:"personal_access_token,omitempty"` + Project string `yaml:"project,omitempty"` + IssueType string `yaml:"issue_type,omitempty"` + Summary string `yaml:"summary,omitempty"` + ReopenState string `yaml:"reopen_state,omitempty"` + ReopenDuration string `yaml:"reopen_duration,omitempty"` + + Priority string `yaml:"priority,omitempty"` + Description string `yaml:"description,omitempty"` + WontFixResolution string `yaml:"wont_fix_resolution,omitempty"` + AddGroupLabels bool `yaml:"add_group_labels,omitempty"` + + // TODO(rporres): Add support for these. + // Fields map[string]interface{} `yaml:"fields,omitempty"` + // Components []string `yaml:"components,omitempty"` +} + +// A test version of the Config struct to create test yaml fixtures. +type testConfig struct { + Defaults *receiverTestConfig `yaml:"defaults,omitempty"` + Receivers []*receiverTestConfig `yaml:"receivers,omitempty"` + Template string `yaml:"template,omitempty"` +} + +// Required Config keys tests. +func TestMissingConfigKeys(t *testing.T) { + defaultsConfig := newReceiverTestConfig(mandatoryReceiverFields(), []string{}) + receiverConfig := newReceiverTestConfig([]string{"Name"}, []string{}) + + var config testConfig + + // No receivers. + config = testConfig{Defaults: defaultsConfig, Receivers: []*receiverTestConfig{}, Template: "jiralert.tmpl"} + configErrorTestRunner(t, config, "no receivers defined") + + // No template. + config = testConfig{Defaults: defaultsConfig, Receivers: []*receiverTestConfig{receiverConfig}} + configErrorTestRunner(t, config, "missing template file") +} + +// Tests regarding mandatory keys. +// No tests for auth keys here. They will be handled separately. +func TestRequiredReceiverConfigKeys(t *testing.T) { + mandatory := mandatoryReceiverFields() + for _, test := range []struct { + missingField string + errorMessage string + }{ + {"Name", "missing name for receiver"}, + {"APIURL", `missing api_url in receiver "Name"`}, + {"Project", `missing project in receiver "Name"`}, + {"IssueType", `missing issue_type in receiver "Name"`}, + {"Summary", `missing summary in receiver "Name"`}, + {"ReopenState", `missing reopen_state in receiver "Name"`}, + {"ReopenDuration", `missing reopen_duration in receiver "Name"`}, + } { + + fields := removeFromStrSlice(mandatory, test.missingField) + + // Non-empty defaults as we don't handle the empty defaults case yet. + defaultsConfig := newReceiverTestConfig([]string{}, []string{"Priority"}) + receiverConfig := newReceiverTestConfig(fields, []string{}) + config := testConfig{ + Defaults: defaultsConfig, + Receivers: []*receiverTestConfig{receiverConfig}, + Template: "jiratemplate.tmpl", + } + configErrorTestRunner(t, config, test.errorMessage) + } + +} + +// Auth keys error scenarios. +func TestAuthKeysErrors(t *testing.T) { + mandatory := mandatoryReceiverFields() + minimalReceiverTestConfig := newReceiverTestConfig([]string{"Name"}, []string{}) + + // Test cases: + // * missing user. + // * missing password. + // * specifying user and PAT auth. + // * specifying password and PAT auth. + // * specifying user, password and PAT auth. + for _, test := range []struct { + receiverTestConfigMandatoryFields []string + errorMessage string + }{ + { + removeFromStrSlice(mandatory, "User"), + `missing authentication in receiver "Name"`, + }, + { + removeFromStrSlice(mandatory, "Password"), + `missing authentication in receiver "Name"`, + }, + { + append(removeFromStrSlice(mandatory, "Password"), "PersonalAccessToken"), + "bad auth config in defaults section: user/password and PAT authentication are mutually exclusive", + }, + + { + append(removeFromStrSlice(mandatory, "User"), "PersonalAccessToken"), + "bad auth config in defaults section: user/password and PAT authentication are mutually exclusive", + }, + { + append(mandatory, "PersonalAccessToken"), + "bad auth config in defaults section: user/password and PAT authentication are mutually exclusive", + }, + } { + + defaultsConfig := newReceiverTestConfig(test.receiverTestConfigMandatoryFields, []string{}) + config := testConfig{ + Defaults: defaultsConfig, + Receivers: []*receiverTestConfig{minimalReceiverTestConfig}, + Template: "jiralert.tmpl", + } + + configErrorTestRunner(t, config, test.errorMessage) + } +} + +// These tests want to make sure that receiver auth always overrides defaults auth. +func TestAuthKeysOverrides(t *testing.T) { + defaultsWithUserPassword := mandatoryReceiverFields() + + defaultsWithPAT := []string{"PersonalAccessToken"} + for _, field := range defaultsWithUserPassword { + if field == "User" || field == "Password" { + continue + } + defaultsWithPAT = append(defaultsWithPAT, field) + } + + // Test cases: + // * user receiver overrides user default. + // * password receiver overrides password default. + // * user & password receiver overrides user & password default. + // * PAT receiver overrides user & password default. + // * PAT receiver overrides PAT default. + // * user/password receiver overrides PAT default. + for _, test := range []struct { + userOverrideValue string + passwordOverrideValue string + patOverrideValue string // Personal Access Token override. + userExpectedValue string + passwordExpectedValue string + patExpectedValue string + defaultFields []string // Fields to build the config defaults. + }{ + {"jiraUser", "", "", "jiraUser", "Password", "", defaultsWithUserPassword}, + {"", "jiraPass", "", "User", "jiraPass", "", defaultsWithUserPassword}, + {"jiraUser", "jiraPass", "", "jiraUser", "jiraPass", "", defaultsWithUserPassword}, + {"", "", "jiraPAT", "", "", "jiraPAT", defaultsWithUserPassword}, + {"jiraUser", "jiraPass", "", "jiraUser", "jiraPass", "", defaultsWithPAT}, + {"", "", "jiraPAT", "", "", "jiraPAT", defaultsWithPAT}, + } { + defaultsConfig := newReceiverTestConfig(test.defaultFields, []string{}) + receiverConfig := newReceiverTestConfig([]string{"Name"}, []string{}) + if test.userOverrideValue != "" { + receiverConfig.User = test.userOverrideValue + } + if test.passwordOverrideValue != "" { + receiverConfig.Password = test.passwordOverrideValue + } + if test.patOverrideValue != "" { + receiverConfig.PersonalAccessToken = test.patOverrideValue + } + + config := testConfig{ + Defaults: defaultsConfig, + Receivers: []*receiverTestConfig{receiverConfig}, + Template: "jiralert.tmpl", + } + + yamlConfig, err := yaml.Marshal(&config) + require.NoError(t, err) + + cfg, err := Load(string(yamlConfig)) + require.NoError(t, err) + + receiver := cfg.Receivers[0] + require.Equal(t, receiver.User, test.userExpectedValue) + require.Equal(t, receiver.Password, Secret(test.passwordExpectedValue)) + require.Equal(t, receiver.PersonalAccessToken, Secret(test.patExpectedValue)) + } +} + +// Tests regarding yaml keys overriden in the receiver config. +// No tests for auth keys here. They will be handled separately +func TestReceiverOverrides(t *testing.T) { + fifteenHoursToDuration, err := ParseDuration("15h") + require.NoError(t, err) + + // We'll override one key at a time and check the value in the receiver. + for _, test := range []struct { + overrideField string + overrideValue interface{} + expectedValue interface{} + }{ + {"APIURL", `https://jira.redhat.com`, `https://jira.redhat.com`}, + {"Project", "APPSRE", "APPSRE"}, + {"IssueType", "Task", "Task"}, + {"Summary", "A nice summary", "A nice summary"}, + {"ReopenState", "To Do", "To Do"}, + {"ReopenDuration", "15h", &fifteenHoursToDuration}, + {"Priority", "Critical", "Critical"}, + {"Description", "A nice description", "A nice description"}, + {"WontFixResolution", "Won't Fix", "Won't Fix"}, + {"AddGroupLabels", false, false}, + } { + optionalFields := []string{"Priority", "Description", "WontFixResolution", "AddGroupLabels"} + defaultsConfig := newReceiverTestConfig(mandatoryReceiverFields(), optionalFields) + receiverConfig := newReceiverTestConfig([]string{"Name"}, optionalFields) + + reflect.ValueOf(receiverConfig).Elem().FieldByName(test.overrideField). + Set(reflect.ValueOf(test.overrideValue)) + + config := testConfig{ + Defaults: defaultsConfig, + Receivers: []*receiverTestConfig{receiverConfig}, + Template: "jiralert.tmpl", + } + + yamlConfig, err := yaml.Marshal(&config) + require.NoError(t, err) + + cfg, err := Load(string(yamlConfig)) + require.NoError(t, err) + + receiver := cfg.Receivers[0] + configValue := reflect.ValueOf(receiver).Elem().FieldByName(test.overrideField).Interface() + require.Equal(t, configValue, test.expectedValue) + } + +} + +// TODO(bwplotka, rporres). Add more tests: +// * Tests on optional keys. +// * Tests on unknown keys. +// * Tests on Duration. + +// Creates a receiverTestConfig struct with default values. +func newReceiverTestConfig(mandatory []string, optional []string) *receiverTestConfig { + r := receiverTestConfig{} + + for _, name := range mandatory { + var value reflect.Value + if name == "APIURL" { + value = reflect.ValueOf("https://jiralert.atlassian.net") + } else if name == "ReopenDuration" { + value = reflect.ValueOf("30d") + } else { + value = reflect.ValueOf(name) + } + + reflect.ValueOf(&r).Elem().FieldByName(name).Set(value) + } + + for _, name := range optional { + var value reflect.Value + if name == "AddGroupLabels" { + value = reflect.ValueOf(true) + } else { + value = reflect.ValueOf(name) + } + + reflect.ValueOf(&r).Elem().FieldByName(name).Set(value) + } + + return &r +} + +// Creates a yaml from testConfig, Loads it checks the errors are the expected ones. +func configErrorTestRunner(t *testing.T, config testConfig, errorMessage string) { + yamlConfig, err := yaml.Marshal(&config) + require.NoError(t, err) + + _, err = Load(string(yamlConfig)) + require.Error(t, err) + require.Contains(t, err.Error(), errorMessage) +} + +// returns a new slice that has the element removed +func removeFromStrSlice(strSlice []string, element string) []string { + var newStrSlice []string + for _, value := range strSlice { + if value != element { + newStrSlice = append(newStrSlice, value) + } + } + + return newStrSlice +} + +// Returns mandatory receiver fields to be used creating test config structs. +// It does not include PAT auth, those tests will be created separately. +func mandatoryReceiverFields() []string { + return []string{"Name", "APIURL", "User", "Password", "Project", + "IssueType", "Summary", "ReopenState", "ReopenDuration"} }