1
0
Fork 0

Adding upstream version 0.8.9.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-22 10:16:14 +02:00
parent 3b2c48b5e4
commit c0c4addb85
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
285 changed files with 25880 additions and 0 deletions

View file

@ -0,0 +1,160 @@
package opsgenie
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/nicholas-fedor/shoutrrr/pkg/format"
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
)
// alertEndpointTemplate is the OpsGenie API endpoint template for sending alerts.
const (
alertEndpointTemplate = "https://%s:%d/v2/alerts"
MaxMessageLength = 130 // MaxMessageLength is the maximum length of the alert message field in OpsGenie.
httpSuccessMax = 299 // httpSuccessMax is the maximum HTTP status code for a successful response.
defaultHTTPTimeout = 10 * time.Second // defaultHTTPTimeout is the default timeout for HTTP requests.
)
// ErrUnexpectedStatus indicates that OpsGenie returned an unexpected HTTP status code.
var ErrUnexpectedStatus = errors.New("OpsGenie notification returned unexpected HTTP status code")
// Service provides OpsGenie as a notification service.
type Service struct {
standard.Standard
Config *Config
pkr format.PropKeyResolver
}
// sendAlert sends an alert to OpsGenie using the specified URL and API key.
func (service *Service) sendAlert(url string, apiKey string, payload AlertPayload) error {
jsonBody, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshaling alert payload to JSON: %w", err)
}
jsonBuffer := bytes.NewBuffer(jsonBody)
ctx, cancel := context.WithTimeout(context.Background(), defaultHTTPTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, jsonBuffer)
if err != nil {
return fmt.Errorf("creating HTTP request: %w", err)
}
req.Header.Add("Authorization", "GenieKey "+apiKey)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send notification to OpsGenie: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode > httpSuccessMax {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf(
"%w: %d, cannot read body: %w",
ErrUnexpectedStatus,
resp.StatusCode,
err,
)
}
return fmt.Errorf("%w: %d - %s", ErrUnexpectedStatus, resp.StatusCode, body)
}
return nil
}
// Initialize configures the service with a URL and logger.
func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error {
service.SetLogger(logger)
service.Config = &Config{}
service.pkr = format.NewPropKeyResolver(service.Config)
return service.Config.setURL(&service.pkr, configURL)
}
// GetID returns the service identifier.
func (service *Service) GetID() string {
return Scheme
}
// Send delivers a notification message to OpsGenie.
// See: https://docs.opsgenie.com/docs/alert-api#create-alert
func (service *Service) Send(message string, params *types.Params) error {
config := service.Config
endpointURL := fmt.Sprintf(alertEndpointTemplate, config.Host, config.Port)
payload, err := service.newAlertPayload(message, params)
if err != nil {
return err
}
return service.sendAlert(endpointURL, config.APIKey, payload)
}
// newAlertPayload creates a new alert payload for OpsGenie based on the message and parameters.
func (service *Service) newAlertPayload(
message string,
params *types.Params,
) (AlertPayload, error) {
if params == nil {
params = &types.Params{}
}
// Defensive copy
payloadFields := *service.Config
if err := service.pkr.UpdateConfigFromParams(&payloadFields, params); err != nil {
return AlertPayload{}, fmt.Errorf("updating payload fields from params: %w", err)
}
// Use `Message` for the title if available, or if the message is too long
// Use `Description` for the message in these scenarios
title := payloadFields.Title
description := message
if title == "" {
if len(message) > MaxMessageLength {
title = message[:MaxMessageLength]
} else {
title = message
description = ""
}
}
if payloadFields.Description != "" && description != "" {
description += "\n"
}
result := AlertPayload{
Message: title,
Alias: payloadFields.Alias,
Description: description + payloadFields.Description,
Responders: payloadFields.Responders,
VisibleTo: payloadFields.VisibleTo,
Actions: payloadFields.Actions,
Tags: payloadFields.Tags,
Details: payloadFields.Details,
Entity: payloadFields.Entity,
Source: payloadFields.Source,
Priority: payloadFields.Priority,
User: payloadFields.User,
Note: payloadFields.Note,
}
return result, nil
}

View file

@ -0,0 +1,109 @@
package opsgenie
import (
"errors"
"fmt"
"net/url"
"strconv"
"github.com/nicholas-fedor/shoutrrr/pkg/format"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
)
const (
defaultPort = 443 // defaultPort is the default port for OpsGenie API connections.
Scheme = "opsgenie" // Scheme is the identifying part of this service's configuration URL.
)
// ErrAPIKeyMissing indicates that the API key is missing from the config URL path.
var ErrAPIKeyMissing = errors.New("API key missing from config URL path")
// Config holds the configuration for the OpsGenie service.
type Config struct {
APIKey string `desc:"The OpsGenie API key" url:"path"`
Host string `desc:"The OpsGenie API host. Use 'api.eu.opsgenie.com' for EU instances" url:"host" default:"api.opsgenie.com"`
Port uint16 `desc:"The OpsGenie API port." url:"port" default:"443"`
Alias string `desc:"Client-defined identifier of the alert" key:"alias" optional:"true"`
Description string `desc:"Description field of the alert" key:"description" optional:"true"`
Responders []Entity `desc:"Teams, users, escalations and schedules that the alert will be routed to send notifications" key:"responders" optional:"true"`
VisibleTo []Entity `desc:"Teams and users that the alert will become visible to without sending any notification" key:"visibleTo" optional:"true"`
Actions []string `desc:"Custom actions that will be available for the alert" key:"actions" optional:"true"`
Tags []string `desc:"Tags of the alert" key:"tags" optional:"true"`
Details map[string]string `desc:"Map of key-value pairs to use as custom properties of the alert" key:"details" optional:"true"`
Entity string `desc:"Entity field of the alert that is generally used to specify which domain the Source field of the alert" key:"entity" optional:"true"`
Source string `desc:"Source field of the alert" key:"source" optional:"true"`
Priority string `desc:"Priority level of the alert. Possible values are P1, P2, P3, P4 and P5" key:"priority" optional:"true"`
Note string `desc:"Additional note that will be added while creating the alert" key:"note" optional:"true"`
User string `desc:"Display name of the request owner" key:"user" optional:"true"`
Title string `desc:"notification title, optionally set by the sender" default:"" key:"title"`
}
// Enums returns an empty map because the OpsGenie service doesn't use Enums.
func (config *Config) Enums() map[string]types.EnumFormatter {
return map[string]types.EnumFormatter{}
}
// GetURL returns a URL representation of the Config's current field values.
func (config *Config) GetURL() *url.URL {
resolver := format.NewPropKeyResolver(config)
return config.getURL(&resolver)
}
// getURL constructs a URL from the Config's fields using the provided resolver.
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
var host string
if config.Port > 0 {
host = fmt.Sprintf("%s:%d", config.Host, config.Port)
} else {
host = config.Host
}
result := &url.URL{
Host: host,
Path: "/" + config.APIKey,
Scheme: Scheme,
RawQuery: format.BuildQuery(resolver),
}
return result
}
// SetURL updates the Config from a URL representation of its field values.
func (config *Config) SetURL(url *url.URL) error {
resolver := format.NewPropKeyResolver(config)
return config.setURL(&resolver, url)
}
// setURL updates the Config from a URL using the provided resolver.
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
config.Host = url.Hostname()
if url.String() != "opsgenie://dummy@dummy.com" {
if len(url.Path) > 0 {
config.APIKey = url.Path[1:]
} else {
return ErrAPIKeyMissing
}
}
if url.Port() != "" {
port, err := strconv.ParseUint(url.Port(), 10, 16)
if err != nil {
return fmt.Errorf("parsing port %q: %w", url.Port(), err)
}
config.Port = uint16(port)
} else {
config.Port = defaultPort
}
for key, vals := range url.Query() {
if err := resolver.Set(key, vals[0]); err != nil {
return fmt.Errorf("setting query parameter %q to %q: %w", key, vals[0], err)
}
}
return nil
}

View file

@ -0,0 +1,93 @@
package opsgenie
import (
"errors"
"fmt"
"regexp"
"strings"
)
// EntityPartsCount is the expected number of parts in an entity string (type:identifier).
const (
EntityPartsCount = 2 // Expected number of parts in an entity string (type:identifier)
)
// ErrInvalidEntityFormat indicates that the entity string does not have two elements separated by a colon.
var (
ErrInvalidEntityFormat = errors.New(
"invalid entity, should have two elements separated by colon",
)
ErrInvalidEntityIDName = errors.New("invalid entity, cannot parse id/name")
ErrUnexpectedEntityType = errors.New("invalid entity, unexpected entity type")
ErrMissingEntityIdentity = errors.New("invalid entity, should have either ID, name or username")
)
// Entity represents an OpsGenie entity (e.g., user, team) with type and identifier.
// Example JSON: { "username":"trinity@opsgenie.com", "type":"user" }.
type Entity struct {
Type string `json:"type"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
}
// SetFromProp deserializes an entity from a string in the format "type:identifier".
func (e *Entity) SetFromProp(propValue string) error {
elements := strings.Split(propValue, ":")
if len(elements) != EntityPartsCount {
return fmt.Errorf("%w: %q", ErrInvalidEntityFormat, propValue)
}
e.Type = elements[0]
identifier := elements[1]
isID, err := isOpsGenieID(identifier)
if err != nil {
return fmt.Errorf("%w: %q", ErrInvalidEntityIDName, identifier)
}
switch {
case isID:
e.ID = identifier
case e.Type == "team":
e.Name = identifier
case e.Type == "user":
e.Username = identifier
default:
return fmt.Errorf("%w: %q", ErrUnexpectedEntityType, e.Type)
}
return nil
}
// GetPropValue serializes an entity back into a string in the format "type:identifier".
func (e *Entity) GetPropValue() (string, error) {
var identifier string
switch {
case e.ID != "":
identifier = e.ID
case e.Name != "":
identifier = e.Name
case e.Username != "":
identifier = e.Username
default:
return "", ErrMissingEntityIdentity
}
return fmt.Sprintf("%s:%s", e.Type, identifier), nil
}
// isOpsGenieID checks if a string matches the OpsGenie ID format (e.g., 4513b7ea-3b91-438f-b7e4-e3e54af9147c).
func isOpsGenieID(str string) (bool, error) {
matched, err := regexp.MatchString(
`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`,
str,
)
if err != nil {
return false, fmt.Errorf("matching OpsGenie ID format for %q: %w", str, err)
}
return matched, nil
}

View file

@ -0,0 +1,33 @@
package opsgenie
// AlertPayload represents the payload being sent to the OpsGenie API
//
// See: https://docs.opsgenie.com/docs/alert-api#create-alert
//
// Some fields contain complex values like arrays and objects.
// Because `params` are strings only we cannot pass in slices
// or maps. Instead we "preserve" the JSON in those fields. That
// way we can pass in complex types as JSON like so:
//
// service.Send("An example alert message", &types.Params{
// "alias": "Life is too short for no alias",
// "description": "Every alert needs a description",
// "responders": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"NOC","type":"team"}]`,
// "visibleTo": `[{"id":"4513b7ea-3b91-438f-b7e4-e3e54af9147c","type":"team"},{"name":"rocket_team","type":"team"}]`,
// "details": `{"key1": "value1", "key2": "value2"}`,
// })
type AlertPayload struct {
Message string `json:"message"`
Alias string `json:"alias,omitempty"`
Description string `json:"description,omitempty"`
Responders []Entity `json:"responders,omitempty"`
VisibleTo []Entity `json:"visibleTo,omitempty"`
Actions []string `json:"actions,omitempty"`
Tags []string `json:"tags,omitempty"`
Details map[string]string `json:"details,omitempty"`
Entity string `json:"entity,omitempty"`
Source string `json:"source,omitempty"`
Priority string `json:"priority,omitempty"`
User string `json:"user,omitempty"`
Note string `json:"note,omitempty"`
}

View file

@ -0,0 +1,422 @@
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&note=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&note=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&note=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`))
})
})
})