diff --git a/notifiers/shoutrrr/new.go b/notifiers/shoutrrr/new.go index f72319bf..8c245e1f 100644 --- a/notifiers/shoutrrr/new.go +++ b/notifiers/shoutrrr/new.go @@ -27,7 +27,7 @@ type TestPayload struct { ServiceName string `json:"service_name"` Name string `json:"name"` NamePrevious string `json:"name_previous"` - Type *string `json:"type,omitempty"` + Type string `json:"type,omitempty"` Options map[string]string `json:"options"` URLFields map[string]string `json:"url_fields"` Params map[string]string `json:"params"` @@ -36,7 +36,7 @@ type TestPayload struct { } // FromPayload will create a Shoutrrr from a payload. -// Copying any undefined values from the previous Service Notify. +// Replacing any undefined values with that of the previous Notify. func FromPayload( payload *TestPayload, serviceNotifies *Slice, @@ -51,21 +51,18 @@ func FromPayload( } name := util.FirstNonDefault(payload.Name, payload.NamePrevious) - nType := util.DefaultIfNil(payload.Type) // Original Notifier? original := &Shoutrrr{} if serviceNotifies != nil && (*serviceNotifies)[payload.NamePrevious] != nil { original = (*serviceNotifies)[payload.NamePrevious] - // Copy that previous Notify Type - if payload.Type == nil { - nType = (*serviceNotifies)[payload.NamePrevious].Type - } + // Copy that previous Notify Type if not set + payload.Type = util.FirstNonDefault(payload.Type, (*serviceNotifies)[payload.NamePrevious].Type) } // Get the Type, Main, Defaults, and HardDefaults for this Notify nType, main, dfault, hardDefault, err := sortDefaults( - name, nType, + name, payload.Type, mains[name], defaults, hardDefaults) if err != nil { return diff --git a/notifiers/shoutrrr/new_test.go b/notifiers/shoutrrr/new_test.go index 641b2da3..92af3d12 100644 --- a/notifiers/shoutrrr/new_test.go +++ b/notifiers/shoutrrr/new_test.go @@ -116,7 +116,7 @@ func TestShoutrrr_FromPayload(t *testing.T) { payload: TestPayload{ ServiceName: "test", Name: "no_main_no_type", - Type: &typeWithNoDefaults, + Type: typeWithNoDefaults, URLFields: typeWithNoDefaultsURLFields}, want: &Shoutrrr{ ShoutrrrBase: ShoutrrrBase{ @@ -144,7 +144,7 @@ func TestShoutrrr_FromPayload(t *testing.T) { payload: TestPayload{ ServiceName: "test", Name: "no_main_with_type_and_defaults", - Type: &typeWithDefaults, + Type: typeWithDefaults, URLFields: typeWithDefaultsURLFields}, want: &Shoutrrr{ ShoutrrrBase: ShoutrrrBase{ @@ -155,7 +155,7 @@ func TestShoutrrr_FromPayload(t *testing.T) { payload: TestPayload{ ServiceName: "test", Name: "main_no_type", - Type: &typeWithNoDefaults}, + Type: typeWithNoDefaults}, want: &Shoutrrr{ ShoutrrrBase: ShoutrrrBase{ Type: typeWithNoDefaults}}, @@ -170,7 +170,7 @@ func TestShoutrrr_FromPayload(t *testing.T) { payload: TestPayload{ ServiceName: "test", Name: "main_with_type_and_defaults", - Type: &typeWithDefaults}, + Type: typeWithDefaults}, want: &Shoutrrr{ ShoutrrrBase: ShoutrrrBase{ Type: typeWithDefaults}}, @@ -179,7 +179,7 @@ func TestShoutrrr_FromPayload(t *testing.T) { payload: TestPayload{ ServiceName: "test", Name: "main_with_type_and_defaults", - Type: &typeWithNoDefaults}, + Type: typeWithNoDefaults}, err: `type: "[^"]+" != "[^"]+"`, }, "edit, have Main, have Defaults - Fail, Invalid field": { @@ -222,7 +222,7 @@ func TestShoutrrr_FromPayload(t *testing.T) { payload: TestPayload{ ServiceName: "test", Name: "main_not_on_service_with_defaults", - Type: &typeWithNoDefaults}, + Type: typeWithNoDefaults}, err: `type: "[^"]+" != "[^"]+"`, }, } diff --git a/test/main_test.go b/test/main_test.go new file mode 100644 index 00000000..f42bc0f7 --- /dev/null +++ b/test/main_test.go @@ -0,0 +1,270 @@ +// Copyright [2024] [Argus] +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build unit + +package test + +import "testing" + +func TestBoolPtr(t *testing.T) { + // GIVEN a boolean value + tests := map[string]struct { + val bool + }{ + "true": {val: true}, + "false": {val: false}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // WHEN BoolPtr is called + result := BoolPtr(tc.val) + + // THEN the result should be a pointer to the boolean value + if *result != tc.val { + t.Errorf("expected %t but got %t", + tc.val, *result) + } + }) + } +} + +func TestIntPtr(t *testing.T) { + // GIVEN an integer value + tests := map[string]struct { + val int + }{ + "positive": {val: 1}, + "zero": {val: 0}, + "negative": {val: -1}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // WHEN IntPtr is called + result := IntPtr(tc.val) + + // THEN the result should be a pointer to the integer value + if *result != tc.val { + t.Errorf("expected %d but got %d", + tc.val, *result) + } + }) + } +} + +func TestStringPtr(t *testing.T) { + // GIVEN a string value + tests := map[string]struct { + val string + }{ + "empty": {val: ""}, + "non-empty": {val: "hello"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // WHEN StringPtr is called + result := StringPtr(tc.val) + + // THEN the result should be a pointer to the string value + if *result != tc.val { + t.Errorf("expected %q but got %q", + tc.val, *result) + } + }) + } +} + +func TestUIntPtr(t *testing.T) { + // GIVEN an integer value + tests := map[string]struct { + val uint + }{ + "positive": {val: 1}, + "zero": {val: 0}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // WHEN UIntPtr is called + result := UIntPtr(int(tc.val)) + + // THEN the result should be a pointer to the unsigned integer value + if *result != uint(tc.val) { + t.Errorf("expected %d but got %d", + tc.val, *result) + } + }) + } +} + +func TestStringifyPtr(t *testing.T) { + // GIVEN a pointer to a value + tests := map[string]struct { + ptr interface{} + want string + }{ + "nil": {ptr: nil, want: "nil"}, + "int, positive": {ptr: IntPtr(1), want: "1"}, + "int, negative": {ptr: IntPtr(-1), want: "-1"}, + "string": {ptr: StringPtr("hello"), want: "hello"}, + "uint": {ptr: UIntPtr(1), want: "1"}, + "bool": {ptr: BoolPtr(true), want: "true"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // WHEN StringifyPtr is called + var result string + switch v := tc.ptr.(type) { + case *bool: + result = StringifyPtr(v) + case *int: + result = StringifyPtr(v) + case *string: + result = StringifyPtr(v) + case *uint: + result = StringifyPtr(v) + case nil: + var nilPtr *int + result = StringifyPtr(nilPtr) + default: + t.Fatalf("unexpected type %T", + tc.ptr) + } + + // THEN the result should be a string representation of the value + if result != tc.want { + t.Errorf("expected %q but got %q", + tc.want, result) + } + }) + } +} + +func TestCopyMapPtr(t *testing.T) { + // GIVEN a map + tests := map[string]struct { + tgt map[string]string + want map[string]string + }{ + "nil": { + tgt: nil, + want: nil, + }, + "empty": { + tgt: map[string]string{}, + want: map[string]string{}, + }, + "non-empty": { + tgt: map[string]string{ + "key": "value", + }, + want: map[string]string{ + "key": "value", + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // WHEN CopyMapPtr is called + result := CopyMapPtr(tc.tgt) + + // THEN the result should be a pointer to a copy of the map + if len(*result) != len(tc.want) { + t.Errorf("length differs, expected %d but got %d", + len(tc.want), len(*result)) + } + for k, v := range tc.want { + if (*result)[k] != v { + t.Errorf("%q: expected %q but got %q", + k, v, (*result)[k]) + } + } + }) + } +} + +func TestTrimJSON(t *testing.T) { + // GIVEN a JSON string + tests := map[string]struct { + str string + want string + }{ + "empty": { + str: "", + want: "", + }, + "single line": { + str: `{"key": "value"}`, + want: `{"key":"value"}`, + }, + "multi line": { + str: ` +{ +"key": "value" +}`, + want: `{"key":"value"}`, + }, + "with tabs": { + str: `{ + "key": "value" + }`, + want: `{"key":"value"}`, + }, + "with spaces": { + str: `{ + "key": "value" + }`, + want: `{"key":"value"}`, + }, + "mixed": { + str: `{ + "key": "value", + "key2": "value2" + }`, + want: `{"key":"value","key2":"value2"}`, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // WHEN TrimJSON is called + result := TrimJSON(tc.str) + + // THEN the result should be the JSON string without newlines and tabs + if result != tc.want { + t.Errorf("expected %q but got %q", + tc.want, result) + } + }) + } +} diff --git a/test/shoutrrr.go b/test/shoutrrr.go index fe8b561d..5b701a34 100644 --- a/test/shoutrrr.go +++ b/test/shoutrrr.go @@ -33,7 +33,7 @@ func testShoutrrrrGotifyToken() (token string) { return } -func TestShoutrrrDefaults(failing bool, selfSignedCert bool) *shoutrrr.ShoutrrrDefaults { +func ShoutrrrDefaults(failing bool, selfSignedCert bool) *shoutrrr.ShoutrrrDefaults { url := "valid.release-argus.io" if selfSignedCert { url = strings.Replace(url, "valid", "invalid", 1) @@ -53,7 +53,7 @@ func TestShoutrrrDefaults(failing bool, selfSignedCert bool) *shoutrrr.ShoutrrrD return shoutrrr } -func TestShoutrrr(failing bool, selfSignedCert bool) *shoutrrr.Shoutrrr { +func Shoutrrr(failing bool, selfSignedCert bool) *shoutrrr.Shoutrrr { url := "valid.release-argus.io" if selfSignedCert { url = strings.Replace(url, "valid", "invalid", 1) diff --git a/test/shoutrrr_test.go b/test/shoutrrr_test.go new file mode 100644 index 00000000..129fef19 --- /dev/null +++ b/test/shoutrrr_test.go @@ -0,0 +1,166 @@ +// Copyright [2024] [Argus] +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build unit + +package test + +import ( + "os" + "testing" +) + +func TestShoutrrrrGotifyToken(t *testing.T) { + // GIVEN the environment variable ARGUS_TEST_GOTIFY_TOKEN + tests := map[string]struct { + env string + }{ + "empty": {env: ""}, + "set": {env: "test"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // t.Parallel() - not parallel because we are manipulating the environment + + want := "AGE-LlHU89Q56uQ" + if tc.env != "" { + os.Setenv("ARGUS_TEST_GOTIFY_TOKEN", tc.env) + defer os.Unsetenv("ARGUS_TEST_GOTIFY_TOKEN") + want = tc.env + } + + // WHEN testShoutrrrrGotifyToken is called + token := testShoutrrrrGotifyToken() + + // THEN the token should be as expected + if token != want { + t.Errorf("expected %q but got %q", + want, token) + } + }) + } +} + +func TestShoutrrrDefaults(t *testing.T) { + // GIVEN the failing and self-signed certificate flags + tests := map[string]struct { + failing bool + selfSignedCert bool + }{ + "passing, signed": {failing: false, selfSignedCert: false}, + "passing, self-signed": {failing: false, selfSignedCert: true}, + "failing, signed": {failing: true, selfSignedCert: false}, + "failing, self-signed": {failing: true, selfSignedCert: true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + wantToken := "AGE-LlHU89Q56uQ" + if tc.failing { + wantToken = "invalid" + } + wantHost := "valid.release-argus.io" + if tc.selfSignedCert { + wantHost = "invalid.release-argus.io" + } + + // WHEN ShoutrrrDefaults is called + got := ShoutrrrDefaults(tc.failing, tc.selfSignedCert) + + // THEN the token should be as expected + if got.URLFields["token"] != wantToken { + t.Errorf("expected %q but got %q", + wantToken, got.URLFields["token"]) + } + // AND the host should be as expected + if wantHost != got.URLFields["host"] { + t.Errorf("expected %q but got %q", + wantHost, got.URLFields["host"]) + } + }) + } +} + +func TestShoutrrr(t *testing.T) { + // GIVEN the failing and self-signed certificate flags + tests := map[string]struct { + failing bool + selfSignedCert bool + }{ + "passing, signed": {failing: false, selfSignedCert: false}, + "passing, self-signed": {failing: false, selfSignedCert: true}, + "failing, signed": {failing: true, selfSignedCert: false}, + "failing, self-signed": {failing: true, selfSignedCert: true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + wantToken := "AGE-LlHU89Q56uQ" + if tc.failing { + wantToken = "invalid" + } + wantHost := "valid.release-argus.io" + if tc.selfSignedCert { + wantHost = "invalid.release-argus.io" + } + + // WHEN Shoutrrr is called + got := Shoutrrr(tc.failing, tc.selfSignedCert) + + // THEN the token should be as expected + if wantToken != got.URLFields["token"] { + t.Errorf("expected %q but got %q", + wantToken, got.URLFields["token"]) + } + // AND the host should be as expected + if wantHost != got.URLFields["host"] { + t.Errorf("expected %q but got %q", + wantHost, got.URLFields["host"]) + } + // AND the maps should be initialised + if got.Options == nil { + t.Error("Options map not initialised") + } + if got.URLFields == nil { + t.Error("URLFields map not initialised") + } + if got.Params == nil { + t.Error("Params map not initialised") + } + // AND the defaults should be set + if got.Main == nil { + t.Error("Main not set") + } + if got.Defaults == nil { + t.Error("Defaults not set") + } + if got.HardDefaults == nil { + t.Error("HardDefaults not set") + } + // AND the fails are initialised and set + if got.ServiceStatus == nil || got.Failed == nil { + if got.ServiceStatus == nil { + t.Error("ServiceStatus not set") + } else { + t.Error("Failed not set") + } + } + }) + } +} diff --git a/web/api/v1/http-api-edit_test.go b/web/api/v1/http-api-edit_test.go index 378c5aa7..4b997733 100644 --- a/web/api/v1/http-api-edit_test.go +++ b/web/api/v1/http-api-edit_test.go @@ -821,7 +821,7 @@ func TestHTTP_NotifyTest(t *testing.T) { file := "TestHTTP_NotifyTest.yml" api := testAPI(file) defer os.RemoveAll(file) - validNotify := test.TestShoutrrr(false, false) + validNotify := test.Shoutrrr(false, false) api.Config.Notify = shoutrrr.SliceDefaults{} api.Config.Notify["test"] = shoutrrr.NewDefaults( "gotify", @@ -829,8 +829,8 @@ func TestHTTP_NotifyTest(t *testing.T) { test.CopyMapPtr(validNotify.Params), test.CopyMapPtr(validNotify.URLFields)) api.Config.Service["test"].Notify = map[string]*shoutrrr.Shoutrrr{ - "test": test.TestShoutrrr(false, false), - "no_main": test.TestShoutrrr(false, false)} + "test": test.Shoutrrr(false, false), + "no_main": test.Shoutrrr(false, false)} tests := map[string]struct { queryParams map[string]string payload string