422 lines
15 KiB
Go
422 lines
15 KiB
Go
package opsgenie
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/onsi/ginkgo/v2"
|
|
"github.com/onsi/gomega"
|
|
|
|
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
|
)
|
|
|
|
const (
|
|
mockAPIKey = "eb243592-faa2-4ba2-a551q-1afdf565c889"
|
|
mockHost = "api.opsgenie.com"
|
|
)
|
|
|
|
func TestOpsGenie(t *testing.T) {
|
|
gomega.RegisterFailHandler(ginkgo.Fail)
|
|
ginkgo.RunSpecs(t, "Shoutrrr OpsGenie Suite")
|
|
}
|
|
|
|
var _ = ginkgo.Describe("the OpsGenie service", func() {
|
|
var (
|
|
// a simulated http server to mock out OpsGenie itself
|
|
mockServer *httptest.Server
|
|
// the host of our mock server
|
|
mockHost string
|
|
// function to check if the http request received by the mock server is as expected
|
|
checkRequest func(body string, header http.Header)
|
|
// the shoutrrr OpsGenie service
|
|
service *Service
|
|
// just a mock logger
|
|
mockLogger *log.Logger
|
|
)
|
|
|
|
ginkgo.BeforeEach(func() {
|
|
// Initialize a mock http server
|
|
httpHandler := func(_ http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
defer r.Body.Close()
|
|
|
|
checkRequest(string(body), r.Header)
|
|
}
|
|
mockServer = httptest.NewTLSServer(http.HandlerFunc(httpHandler))
|
|
|
|
// Our mock server doesn't have a valid cert
|
|
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
|
|
// Determine the host of our mock http server
|
|
mockServerURL, err := url.Parse(mockServer.URL)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
mockHost = mockServerURL.Host
|
|
|
|
// Initialize a mock logger
|
|
var buf bytes.Buffer
|
|
mockLogger = log.New(&buf, "", 0)
|
|
})
|
|
|
|
ginkgo.AfterEach(func() {
|
|
mockServer.Close()
|
|
})
|
|
|
|
ginkgo.Context("without query parameters", func() {
|
|
ginkgo.BeforeEach(func() {
|
|
// Initialize service
|
|
serviceURL, err := url.Parse(fmt.Sprintf("opsgenie://%s/%s", mockHost, mockAPIKey))
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
service = &Service{}
|
|
err = service.Initialize(serviceURL, mockLogger)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
})
|
|
|
|
ginkgo.When("sending a simple alert", func() {
|
|
ginkgo.It("should send a request to our mock OpsGenie server", func() {
|
|
checkRequest = func(body string, header http.Header) {
|
|
gomega.Expect(header["Authorization"][0]).
|
|
To(gomega.Equal("GenieKey " + mockAPIKey))
|
|
gomega.Expect(header["Content-Type"][0]).To(gomega.Equal("application/json"))
|
|
gomega.Expect(body).To(gomega.Equal(`{"message":"hello world"}`))
|
|
}
|
|
|
|
err := service.Send("hello world", &types.Params{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
})
|
|
})
|
|
|
|
ginkgo.When("sending an alert with runtime parameters", func() {
|
|
ginkgo.It(
|
|
"should send a request to our mock OpsGenie server with all fields populated from runtime parameters",
|
|
func() {
|
|
checkRequest = func(body string, header http.Header) {
|
|
gomega.Expect(header["Authorization"][0]).
|
|
To(gomega.Equal("GenieKey " + mockAPIKey))
|
|
gomega.Expect(header["Content-Type"][0]).
|
|
To(gomega.Equal("application/json"))
|
|
gomega.Expect(body).To(gomega.Equal(`{"` +
|
|
`message":"An example alert message",` +
|
|
`"alias":"Life is too short for no alias",` +
|
|
`"description":"Every alert needs a description",` +
|
|
`"responders":[{"type":"team","id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c"},{"type":"team","name":"NOC"},{"type":"user","username":"Donald"},{"type":"user","id":"696f0759-3b0f-4a15-b8c8-19d3dfca33f2"}],` +
|
|
`"visibleTo":[{"type":"team","name":"rocket"}],` +
|
|
`"actions":["action1","action2"],` +
|
|
`"tags":["tag1","tag2"],` +
|
|
`"details":{"key1":"value1","key2":"value2"},` +
|
|
`"entity":"An example entity",` +
|
|
`"source":"The source",` +
|
|
`"priority":"P1",` +
|
|
`"user":"Dracula",` +
|
|
`"note":"Here is a note"` +
|
|
`}`))
|
|
}
|
|
|
|
err := service.Send("An example alert message", &types.Params{
|
|
"alias": "Life is too short for no alias",
|
|
"description": "Every alert needs a description",
|
|
"responders": "team:4513b7ea-3b91-438f-b7e4-e3e54af9147c,team:NOC,user:Donald,user:696f0759-3b0f-4a15-b8c8-19d3dfca33f2",
|
|
"visibleTo": "team:rocket",
|
|
"actions": "action1,action2",
|
|
"tags": "tag1,tag2",
|
|
"details": "key1:value1,key2:value2",
|
|
"entity": "An example entity",
|
|
"source": "The source",
|
|
"priority": "P1",
|
|
"user": "Dracula",
|
|
"note": "Here is a note",
|
|
})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
},
|
|
)
|
|
})
|
|
})
|
|
|
|
ginkgo.Context("with query parameters", func() {
|
|
ginkgo.BeforeEach(func() {
|
|
// Initialize service
|
|
serviceURL, err := url.Parse(
|
|
fmt.Sprintf(
|
|
`opsgenie://%s/%s?alias=query-alias&description=query-description&responders=team:query_team&visibleTo=user:query_user&actions=queryAction1,queryAction2&tags=queryTag1,queryTag2&details=queryKey1:queryValue1,queryKey2:queryValue2&entity=query-entity&source=query-source&priority=P2&user=query-user¬e=query-note`,
|
|
mockHost,
|
|
mockAPIKey,
|
|
),
|
|
)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
service = &Service{}
|
|
err = service.Initialize(serviceURL, mockLogger)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
})
|
|
|
|
ginkgo.When("sending a simple alert", func() {
|
|
ginkgo.It(
|
|
"should send a request to our mock OpsGenie server with all fields populated from query parameters",
|
|
func() {
|
|
checkRequest = func(body string, header http.Header) {
|
|
gomega.Expect(header["Authorization"][0]).
|
|
To(gomega.Equal("GenieKey " + mockAPIKey))
|
|
gomega.Expect(header["Content-Type"][0]).
|
|
To(gomega.Equal("application/json"))
|
|
gomega.Expect(body).To(gomega.Equal(`{` +
|
|
`"message":"An example alert message",` +
|
|
`"alias":"query-alias",` +
|
|
`"description":"query-description",` +
|
|
`"responders":[{"type":"team","name":"query_team"}],` +
|
|
`"visibleTo":[{"type":"user","username":"query_user"}],` +
|
|
`"actions":["queryAction1","queryAction2"],` +
|
|
`"tags":["queryTag1","queryTag2"],` +
|
|
`"details":{"queryKey1":"queryValue1","queryKey2":"queryValue2"},` +
|
|
`"entity":"query-entity",` +
|
|
`"source":"query-source",` +
|
|
`"priority":"P2",` +
|
|
`"user":"query-user",` +
|
|
`"note":"query-note"` +
|
|
`}`))
|
|
}
|
|
|
|
err := service.Send("An example alert message", &types.Params{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
},
|
|
)
|
|
})
|
|
|
|
ginkgo.When("sending two alerts", func() {
|
|
ginkgo.It("should not mix-up the runtime parameters and the query parameters", func() {
|
|
// Internally the opsgenie service copies runtime parameters into the config struct
|
|
// before generating the alert payload. This test ensures that none of the parameters
|
|
// from alert 1 remain in the config struct when sending alert 2
|
|
// In short: This tests if we clone the config struct
|
|
|
|
checkRequest = func(body string, header http.Header) {
|
|
gomega.Expect(header["Authorization"][0]).
|
|
To(gomega.Equal("GenieKey " + mockAPIKey))
|
|
gomega.Expect(header["Content-Type"][0]).To(gomega.Equal("application/json"))
|
|
gomega.Expect(body).To(gomega.Equal(`{"` +
|
|
`message":"1",` +
|
|
`"alias":"1",` +
|
|
`"description":"1",` +
|
|
`"responders":[{"type":"team","name":"1"}],` +
|
|
`"visibleTo":[{"type":"team","name":"1"}],` +
|
|
`"actions":["action1","action2"],` +
|
|
`"tags":["tag1","tag2"],` +
|
|
`"details":{"key1":"value1","key2":"value2"},` +
|
|
`"entity":"1",` +
|
|
`"source":"1",` +
|
|
`"priority":"P1",` +
|
|
`"user":"1",` +
|
|
`"note":"1"` +
|
|
`}`))
|
|
}
|
|
|
|
err := service.Send("1", &types.Params{
|
|
"alias": "1",
|
|
"description": "1",
|
|
"responders": "team:1",
|
|
"visibleTo": "team:1",
|
|
"actions": "action1,action2",
|
|
"tags": "tag1,tag2",
|
|
"details": "key1:value1,key2:value2",
|
|
"entity": "1",
|
|
"source": "1",
|
|
"priority": "P1",
|
|
"user": "1",
|
|
"note": "1",
|
|
})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
checkRequest = func(body string, header http.Header) {
|
|
gomega.Expect(header["Authorization"][0]).
|
|
To(gomega.Equal("GenieKey " + mockAPIKey))
|
|
gomega.Expect(header["Content-Type"][0]).To(gomega.Equal("application/json"))
|
|
gomega.Expect(body).To(gomega.Equal(`{` +
|
|
`"message":"2",` +
|
|
`"alias":"query-alias",` +
|
|
`"description":"query-description",` +
|
|
`"responders":[{"type":"team","name":"query_team"}],` +
|
|
`"visibleTo":[{"type":"user","username":"query_user"}],` +
|
|
`"actions":["queryAction1","queryAction2"],` +
|
|
`"tags":["queryTag1","queryTag2"],` +
|
|
`"details":{"queryKey1":"queryValue1","queryKey2":"queryValue2"},` +
|
|
`"entity":"query-entity",` +
|
|
`"source":"query-source",` +
|
|
`"priority":"P2",` +
|
|
`"user":"query-user",` +
|
|
`"note":"query-note"` +
|
|
`}`))
|
|
}
|
|
|
|
err = service.Send("2", nil)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
})
|
|
})
|
|
})
|
|
|
|
ginkgo.It("should return the correct service ID", func() {
|
|
service := &Service{}
|
|
gomega.Expect(service.GetID()).To(gomega.Equal("opsgenie"))
|
|
})
|
|
})
|
|
|
|
var _ = ginkgo.Describe("the OpsGenie Config struct", func() {
|
|
ginkgo.When("generating a config from a simple URL", func() {
|
|
ginkgo.It("should populate the config with host and apikey", func() {
|
|
url, err := url.Parse(fmt.Sprintf("opsgenie://%s/%s", mockHost, mockAPIKey))
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
config := Config{}
|
|
err = config.SetURL(url)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Expect(config.APIKey).To(gomega.Equal(mockAPIKey))
|
|
gomega.Expect(config.Host).To(gomega.Equal(mockHost))
|
|
gomega.Expect(config.Port).To(gomega.Equal(uint16(443)))
|
|
})
|
|
})
|
|
|
|
ginkgo.When("generating a config from a url with port", func() {
|
|
ginkgo.It("should populate the port field", func() {
|
|
url, err := url.Parse(
|
|
fmt.Sprintf("opsgenie://%s/%s", net.JoinHostPort(mockHost, "12345"), mockAPIKey),
|
|
)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
config := Config{}
|
|
err = config.SetURL(url)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Expect(config.Port).To(gomega.Equal(uint16(12345)))
|
|
})
|
|
})
|
|
|
|
ginkgo.When("generating a config from a url with query parameters", func() {
|
|
ginkgo.It("should populate the config fields with the query parameter values", func() {
|
|
queryParams := `alias=Life+is+too+short+for+no+alias&description=Every+alert+needs+a+description&actions=An+action&tags=tag1,tag2&details=key:value,key2:value2&entity=An+example+entity&source=The+source&priority=P1&user=Dracula¬e=Here+is+a+note&responders=user:Test,team:NOC&visibleTo=user:A+User`
|
|
url, err := url.Parse(
|
|
fmt.Sprintf(
|
|
"opsgenie://%s/%s?%s",
|
|
net.JoinHostPort(mockHost, "12345"),
|
|
mockAPIKey,
|
|
queryParams,
|
|
),
|
|
)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
config := Config{}
|
|
err = config.SetURL(url)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Expect(config.Alias).To(gomega.Equal("Life is too short for no alias"))
|
|
gomega.Expect(config.Description).To(gomega.Equal("Every alert needs a description"))
|
|
gomega.Expect(config.Responders).To(gomega.Equal([]Entity{
|
|
{Type: "user", Username: "Test"},
|
|
{Type: "team", Name: "NOC"},
|
|
}))
|
|
gomega.Expect(config.VisibleTo).To(gomega.Equal([]Entity{
|
|
{Type: "user", Username: "A User"},
|
|
}))
|
|
gomega.Expect(config.Actions).To(gomega.Equal([]string{"An action"}))
|
|
gomega.Expect(config.Tags).To(gomega.Equal([]string{"tag1", "tag2"}))
|
|
gomega.Expect(config.Details).
|
|
To(gomega.Equal(map[string]string{"key": "value", "key2": "value2"}))
|
|
gomega.Expect(config.Entity).To(gomega.Equal("An example entity"))
|
|
gomega.Expect(config.Source).To(gomega.Equal("The source"))
|
|
gomega.Expect(config.Priority).To(gomega.Equal("P1"))
|
|
gomega.Expect(config.User).To(gomega.Equal("Dracula"))
|
|
gomega.Expect(config.Note).To(gomega.Equal("Here is a note"))
|
|
})
|
|
})
|
|
|
|
ginkgo.When("generating a config from a url with differently escaped spaces", func() {
|
|
ginkgo.It("should parse the escaped spaces correctly", func() {
|
|
// Use: '%20', '+' and a normal space
|
|
queryParams := `alias=Life is+too%20short+for+no+alias`
|
|
url, err := url.Parse(
|
|
fmt.Sprintf(
|
|
"opsgenie://%s/%s?%s",
|
|
net.JoinHostPort(mockHost, "12345"),
|
|
mockAPIKey,
|
|
queryParams,
|
|
),
|
|
)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
config := Config{}
|
|
err = config.SetURL(url)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Expect(config.Alias).To(gomega.Equal("Life is too short for no alias"))
|
|
})
|
|
})
|
|
|
|
ginkgo.When("generating a url from a simple config", func() {
|
|
ginkgo.It("should generate a url", func() {
|
|
config := Config{
|
|
Host: "api.opsgenie.com",
|
|
APIKey: "eb243592-faa2-4ba2-a551q-1afdf565c889",
|
|
}
|
|
|
|
url := config.GetURL()
|
|
|
|
gomega.Expect(url.String()).
|
|
To(gomega.Equal("opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889"))
|
|
})
|
|
})
|
|
|
|
ginkgo.When("generating a url from a config with a port", func() {
|
|
ginkgo.It("should generate a url with port", func() {
|
|
config := Config{
|
|
Host: "api.opsgenie.com",
|
|
APIKey: "eb243592-faa2-4ba2-a551q-1afdf565c889",
|
|
Port: 12345,
|
|
}
|
|
|
|
url := config.GetURL()
|
|
|
|
gomega.Expect(url.String()).
|
|
To(gomega.Equal("opsgenie://api.opsgenie.com:12345/eb243592-faa2-4ba2-a551q-1afdf565c889"))
|
|
})
|
|
})
|
|
|
|
ginkgo.When("generating a url from a config with all optional config fields", func() {
|
|
ginkgo.It("should generate a url with query parameters", func() {
|
|
config := Config{
|
|
Host: "api.opsgenie.com",
|
|
APIKey: "eb243592-faa2-4ba2-a551q-1afdf565c889",
|
|
Alias: "Life is too short for no alias",
|
|
Description: "Every alert needs a description",
|
|
Responders: []Entity{
|
|
{Type: "user", Username: "Test"},
|
|
{Type: "team", Name: "NOC"},
|
|
{Type: "team", ID: "4513b7ea-3b91-438f-b7e4-e3e54af9147c"},
|
|
},
|
|
VisibleTo: []Entity{
|
|
{Type: "user", Username: "A User"},
|
|
},
|
|
Actions: []string{"action1", "action2"},
|
|
Tags: []string{"tag1", "tag2"},
|
|
Details: map[string]string{"key": "value"},
|
|
Entity: "An example entity",
|
|
Source: "The source",
|
|
Priority: "P1",
|
|
User: "Dracula",
|
|
Note: "Here is a note",
|
|
}
|
|
|
|
url := config.GetURL()
|
|
gomega.Expect(url.String()).
|
|
To(gomega.Equal(`opsgenie://api.opsgenie.com/eb243592-faa2-4ba2-a551q-1afdf565c889?actions=action1%2Caction2&alias=Life+is+too+short+for+no+alias&description=Every+alert+needs+a+description&details=key%3Avalue&entity=An+example+entity¬e=Here+is+a+note&priority=P1&responders=user%3ATest%2Cteam%3ANOC%2Cteam%3A4513b7ea-3b91-438f-b7e4-e3e54af9147c&source=The+source&tags=tag1%2Ctag2&user=Dracula&visibleto=user%3AA+User`))
|
|
})
|
|
})
|
|
})
|