Skip to content

Commit

Permalink
tests: file channel to ease channel testing
Browse files Browse the repository at this point in the history
First, a new channel was added, printing each received notification
event JSON-encoded line-wise into a file or something file-like.

This file channel was then used in the newly written
TestNotificationRoundTrip integration test to get back events from
Icinga 2 over a FIFO between the host running tests and the
icinga-notifications Docker container.
  • Loading branch information
oxzi committed Jan 4, 2024
1 parent 0b60bc7 commit 16b1839
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 54 deletions.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ FROM docker.io/library/alpine
COPY --from=build /src/icinga-notifications/bin/icinga-notifications-daemon /usr/bin/icinga-notifications-daemon
COPY --from=build /src/icinga-notifications/bin/channel /usr/libexec/icinga-notifications/channel

RUN mkdir /etc/icinga-notifications/
COPY config.example.yml /etc/icinga-notifications/config.yml

RUN apk add tzdata

ARG username=notifications
Expand Down
77 changes: 77 additions & 0 deletions cmd/channel/file/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"encoding/json"
"fmt"
"github.com/icinga/icinga-notifications/internal"
"github.com/icinga/icinga-notifications/pkg/plugin"
"os"
)

// File resource to line-wise append each plugin.NotificationRequest as its JSON representation.
//
// The File.Path will be created, if not existing. If something file-like - a file, a fifo(7), .. - exists, it will be
// appended to.
type File struct {
Path string `json:"path"`

encoder *json.Encoder `json:"-"`
}

func main() {
plugin.RunPlugin(&File{})
}

func (ch *File) SendNotification(req *plugin.NotificationRequest) error {
return ch.encoder.Encode(req)
}

func (ch *File) SetConfig(jsonStr json.RawMessage) error {
err := json.Unmarshal(jsonStr, ch)
if err != nil {
return fmt.Errorf("failed to load config: %s %w", jsonStr, err)
}

if ch.Path == "" {
return fmt.Errorf("the path attribute must be set")
}

if ch.Path == "" {
return fmt.Errorf("file path is empty")
}

f, err := os.OpenFile(ch.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("cannot open file path %s: %w", ch.Path, err)
}

ch.encoder = json.NewEncoder(f)
ch.encoder.SetEscapeHTML(false)

return nil
}

func (ch *File) GetInfo() *plugin.Info {
elements := []*plugin.ConfigOption{
{
Name: "path",
Type: "string",
Label: map[string]string{
"en_US": "File Path",
"de_DE": "Dateipfad",
},
},
}

configAttrs, err := json.Marshal(elements)
if err != nil {
panic(err)
}

return &plugin.Info{
Name: "File",
Version: internal.Version.Version,
Author: "Icinga GmbH",
ConfigAttributes: configAttrs,
}
}
11 changes: 9 additions & 2 deletions tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ module github.com/icinga/icinga-notifications/tests

go 1.21

replace github.com/icinga/icinga-testing => github.com/oxzi/icinga-testing v0.0.0-20231220141937-686d5c9faef9
replace (
github.com/icinga/icinga-notifications => ../
github.com/icinga/icinga-testing => github.com/oxzi/icinga-testing v0.0.0-20240103162208-091308cd619c
)

require (
github.com/icinga/icinga-notifications v0.0.0-20240102102116-0d6f7271c116
github.com/icinga/icinga-testing v0.0.0-20220516144008-9600081b7a69
github.com/jmoiron/sqlx v1.3.5
github.com/stretchr/testify v1.8.4
golang.org/x/sys v0.15.0
)

require (
Expand All @@ -24,17 +29,19 @@ require (
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/icinga/icingadb v1.1.1-0.20230418113126-7c4b947aad3a // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/tools v0.16.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
13 changes: 10 additions & 3 deletions tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/icinga/icingadb v1.1.1-0.20230418113126-7c4b947aad3a h1:NfVdBKa4dhPk7IU8u0MOF6ywi0LDpMkQMGs1j803+3c=
github.com/icinga/icingadb v1.1.1-0.20230418113126-7c4b947aad3a/go.mod h1:zamCKaKn4JJQinctcUyewTSNNXDfpLc0HSbqb+9lTYs=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
Expand All @@ -79,8 +83,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
Expand Down Expand Up @@ -117,8 +122,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/oxzi/icinga-testing v0.0.0-20231220141937-686d5c9faef9 h1:+XijgwcZ3MwRxuJ1v/fK/qILzvNKK2GWlcjo4uTPerg=
github.com/oxzi/icinga-testing v0.0.0-20231220141937-686d5c9faef9/go.mod h1:4xtpVSat274WBBuOF7gDkFhE4OtsrHpmJpL1qMU1vsw=
github.com/oxzi/icinga-testing v0.0.0-20240103162208-091308cd619c h1:+RwoI1rFT11O1fiMqnPlIOHXLZDOO2bu7ZSPpwNPik0=
github.com/oxzi/icinga-testing v0.0.0-20240103162208-091308cd619c/go.mod h1:4xtpVSat274WBBuOF7gDkFhE4OtsrHpmJpL1qMU1vsw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down Expand Up @@ -166,6 +171,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
Expand Down
38 changes: 5 additions & 33 deletions tests/main_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package icingadb_test
package notifications_test

import (
"github.com/icinga/icinga-testing"
"github.com/icinga/icinga-testing/services"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
"testing"
"time"
)

var it *icingatesting.IT
Expand All @@ -24,14 +23,12 @@ func getDatabase(t testing.TB) services.RelationalDatabase {

db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
require.NoError(t, err, "SQL database open")
defer db.Close()
defer func() { _ = db.Close() }()

rows, err := db.Query(`
_, err = db.Exec(`
INSERT INTO source (id, type, name, listener_password_hash)
VALUES (1, 'icinga2', 'Icinga 2', '$2y$10$QU8bJ7cpW1SmoVQ/RndX5O2J5L1PJF7NZ2dlIW7Rv3zUEcbUFg3z2')
`)
require.NoError(t, err, "SQL population query")
_ = rows.Close()
VALUES (1, 'icinga2', 'Icinga 2', '$2y$10$QU8bJ7cpW1SmoVQ/RndX5O2J5L1PJF7NZ2dlIW7Rv3zUEcbUFg3z2')`)
require.NoError(t, err, "populating source table failed")

return rdb
}
Expand All @@ -40,28 +37,3 @@ func getEmptyDatabase(t testing.TB) services.RelationalDatabase {
// Currently, PostgreSQL is the only supported database backend.
return it.PostgresqlDatabaseT(t)
}

func TestBasicFunctionality(t *testing.T) {
rdb := getDatabase(t)
notifications := it.IcingaNotificationsInstanceT(t, rdb)
icinga := it.Icinga2NodeT(t, "master")
icinga.EnableIcingaNotifications(notifications)
icinga.Reload()

t.Run("available_channel_type populated", func(t *testing.T) {
db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
require.NoError(t, err, "SQL database open")
defer db.Close()

for i := 0; i < 5; i++ {
var rows int
err := db.QueryRow(`SELECT COUNT(*) FROM available_channel_type`).Scan(&rows)
require.NoError(t, err, "SQL SELECT FROM available_channel_type query")
if rows > 0 {
return
}
time.Sleep(3 * time.Second)
}
require.Fail(t, "available_channel_type table is still empty")
})
}
87 changes: 87 additions & 0 deletions tests/notification_roundtrip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package notifications_test

import (
"encoding/json"
"github.com/icinga/icinga-notifications/pkg/plugin"
"github.com/icinga/icinga-testing/utils/eventually"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
"os"
"testing"
"time"
)

// TestNotificationRoundTrip instructs an Icinga 2 node to send a notification back for further inspection.
func TestNotificationRoundTrip(t *testing.T) {
fileChFifo := "./tmp/file-fifo"
oldUmask := unix.Umask(0)
require.NoError(t, unix.Mkfifo(fileChFifo, 0666), "mkfifo")
unix.Umask(oldUmask)

rdb := getDatabase(t)
notifications := it.IcingaNotificationsInstanceT(t, rdb)
icinga := it.Icinga2NodeT(t, "master")
icinga.EnableIcingaNotifications(notifications)
require.NoError(t, icinga.Reload(), "icinga.Reload()")

db, err := sqlx.Open(rdb.Driver(), rdb.DSN())
require.NoError(t, err, "SQL database open")
defer func() { require.NoError(t, db.Close(), "db.Close") }()

t.Run("configure channel in database", func(t *testing.T) {
eventually.Require(t, func(t require.TestingT) {
var channelCount int
err := db.QueryRow(`SELECT COUNT(*) FROM available_channel_type WHERE type = 'file'`).Scan(&channelCount)
require.NoError(t, err, "SQL SELECT FROM available_channel_type query")
require.Equal(t, channelCount, 1, "file type missing from available_channel_type")
}, 10*time.Second, time.Second)

_, err := db.Exec(`
INSERT INTO channel (id, name, type, config)
VALUES (1, 'file-fifo', 'file', '{"path":"\/shared\/file-fifo"}');
INSERT INTO contact (id, full_name, username, default_channel_id, color)
VALUES (1, 'icingaadmin', 'icingaadmin', 1, '#000000');
INSERT INTO rule (id, name, is_active) VALUES (1, 'file-fifo', 'y');
INSERT INTO rule_escalation (id, rule_id, position) VALUES (1, 1, 1);
INSERT INTO rule_escalation_recipient (id, rule_escalation_id, contact_id, channel_id) VALUES (1, 1, 1, 1);`)
require.NoError(t, err, "populating tables failed")
})

t.Run("create icinga objects", func(t *testing.T) {
client := icinga.ApiClient()
client.CreateObject(t, "checkcommands", "failure-check", map[string]any{
"templates": []any{"plugin-check-command"},
"attrs": map[string]any{"command": []string{"/bin/false"}},
})
client.CreateHost(t, "test-host", map[string]any{
"attrs": map[string]any{"check_command": "failure-check"},
})
client.CreateService(t, "test-host", "test-service", map[string]any{
"attrs": map[string]any{"check_command": "failure-check"},
})
})

t.Run("read notification back from channel", func(t *testing.T) {
f, err := os.Open(fileChFifo)
require.NoErrorf(t, err, "opening %s for reading", fileChFifo)
defer f.Close()

notificationReqCh := make(chan *plugin.NotificationRequest)
go func() {
var req plugin.NotificationRequest
require.NoError(t, json.NewDecoder(f).Decode(&req), "JSON decoding")
notificationReqCh <- &req
}()

select {
case req := <-notificationReqCh:
require.Contains(t, req.Object.Name, "test-", "name must contain test prefix")

case <-time.After(5 * time.Minute):
require.Fail(t, "no notification was received")
}
})
}
26 changes: 10 additions & 16 deletions tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@

set -eux

test -d ./tmp && rm -r ./tmp
mkdir -p ./tmp/channel

cd ..
export CGO_ENABLED=0
go build -o ./tests/tmp/icinga-notifications-daemon ./cmd/icinga-notifications-daemon
go build -o ./tests/tmp/channel ./cmd/channel/...
unset CGO_ENABLED
cd tests

go test -o ./tmp/icinga-notifications-test -c .

ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY="$(realpath tmp/icinga-notifications-daemon)"
ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR="$(realpath tmp/channel)"
ICINGA_TESTING_NOTIFICATIONS_IMAGE="icinga-notifications:latest"
ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR="$(realpath tmp)"
ICINGA_TESTING_ICINGA_NOTIFICATIONS_SCHEMA_PGSQL="$(realpath ../schema/pgsql/schema.sql)"
export ICINGA_TESTING_ICINGA_NOTIFICATIONS_BINARY
export ICINGA_TESTING_ICINGA_NOTIFICATIONS_CHANNEL_DIR
export ICINGA_TESTING_NOTIFICATIONS_IMAGE
export ICINGA_TESTING_ICINGA_NOTIFICATIONS_SHARED_DIR
export ICINGA_TESTING_ICINGA_NOTIFICATIONS_SCHEMA_PGSQL

docker build -t "$ICINGA_TESTING_NOTIFICATIONS_IMAGE" ..

test -d ./tmp && rm -r ./tmp
mkdir ./tmp

go test -o ./tmp/icinga-notifications-test -c .
exec ./tmp/icinga-notifications-test -icingatesting.debuglog ./tmp/debug.log -test.v

0 comments on commit 16b1839

Please sign in to comment.