Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
3b2c48b5e4
commit
c0c4addb85
285 changed files with 25880 additions and 0 deletions
16
internal/dedupe/dedupe.go
Normal file
16
internal/dedupe/dedupe.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package dedupe
|
||||
|
||||
import "slices"
|
||||
|
||||
// RemoveDuplicates from a slice of strings.
|
||||
func RemoveDuplicates(src []string) []string {
|
||||
unique := make([]string, 0, len(src))
|
||||
for _, s := range src {
|
||||
found := slices.Contains(unique, s)
|
||||
if !found {
|
||||
unique = append(unique, s)
|
||||
}
|
||||
}
|
||||
|
||||
return unique
|
||||
}
|
41
internal/dedupe/dedupe_test.go
Normal file
41
internal/dedupe/dedupe_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package dedupe_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/dedupe"
|
||||
)
|
||||
|
||||
func TestRemoveDuplicates(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input []string
|
||||
want []string
|
||||
}{
|
||||
"no duplicates": {
|
||||
input: []string{"a", "b", "c"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
"duplicate inside slice": {
|
||||
input: []string{"a", "b", "a", "c"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
"duplicate at end of slice": {
|
||||
input: []string{"a", "b", "c", "a"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
"duplicate next to each other inside slice": {
|
||||
input: []string{"a", "b", "b", "c"},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := dedupe.RemoveDuplicates(tc.input)
|
||||
if !reflect.DeepEqual(tc.want, got) {
|
||||
t.Fatalf("expected: %#v, got: %#v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
68
internal/failures/failure.go
Normal file
68
internal/failures/failure.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package failures
|
||||
|
||||
import "fmt"
|
||||
|
||||
// FailureID is a unique identifier for a specific error type.
|
||||
type FailureID int
|
||||
|
||||
// failure is the concrete implementation of the Failure interface.
|
||||
// It wraps an error with a message and an ID for categorization.
|
||||
type failure struct {
|
||||
message string // Descriptive message for the error
|
||||
id FailureID // Unique identifier for this error type
|
||||
wrapped error // Underlying error, if any, for chaining
|
||||
}
|
||||
|
||||
// Failure extends the error interface with an ID and methods for unwrapping and comparison.
|
||||
// It allows errors to be identified by a unique ID and supports Go’s error wrapping conventions.
|
||||
type Failure interface {
|
||||
error
|
||||
ID() FailureID // Returns the unique identifier for this failure
|
||||
Unwrap() error // Returns the wrapped error, if any
|
||||
Is(target error) bool // Checks if the target error matches this failure by ID
|
||||
}
|
||||
|
||||
// Error returns the failure’s message, appending the wrapped error’s message if present.
|
||||
func (f *failure) Error() string {
|
||||
if f.wrapped == nil {
|
||||
return f.message
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: %v", f.message, f.wrapped)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error wrapped by this failure, or nil if none exists.
|
||||
func (f *failure) Unwrap() error {
|
||||
return f.wrapped
|
||||
}
|
||||
|
||||
// ID returns the unique identifier assigned to this failure.
|
||||
func (f *failure) ID() FailureID {
|
||||
return f.id
|
||||
}
|
||||
|
||||
// Is reports whether the target error is a failure with the same ID.
|
||||
// It only returns true for failures of the same type with matching IDs.
|
||||
func (f *failure) Is(target error) bool {
|
||||
targetFailure, ok := target.(*failure)
|
||||
|
||||
return ok && targetFailure.id == f.id
|
||||
}
|
||||
|
||||
// Wrap creates a new failure with the given message, ID, and optional wrapped error.
|
||||
// If variadic arguments are provided, they are used to format the message using fmt.Sprintf.
|
||||
// This supports Go’s error wrapping pattern while adding a unique ID for identification.
|
||||
func Wrap(message string, failureID FailureID, wrappedError error, v ...any) Failure {
|
||||
if len(v) > 0 {
|
||||
message = fmt.Sprintf(message, v...)
|
||||
}
|
||||
|
||||
return &failure{
|
||||
message: message,
|
||||
id: failureID,
|
||||
wrapped: wrappedError,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure failure implements the error interface at compile time.
|
||||
var _ error = &failure{}
|
174
internal/failures/failure_test.go
Normal file
174
internal/failures/failure_test.go
Normal file
|
@ -0,0 +1,174 @@
|
|||
package failures_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/format"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/failures"
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/testutils"
|
||||
)
|
||||
|
||||
// TestFailures runs the Ginkgo test suite for the failures package.
|
||||
func TestFailures(t *testing.T) {
|
||||
format.CharactersAroundMismatchToInclude = 20 // Show more context in failure output
|
||||
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
ginkgo.RunSpecs(t, "Failure Suite")
|
||||
}
|
||||
|
||||
var _ = ginkgo.Describe("the failure package", func() {
|
||||
// Common test fixtures
|
||||
var (
|
||||
testID failures.FailureID = 42 // Consistent ID for testing
|
||||
testMessage = "test failure occurred" // Sample error message
|
||||
wrappedErr = errors.New("underlying error") // Sample wrapped error
|
||||
)
|
||||
|
||||
ginkgo.Describe("Wrap function", func() {
|
||||
ginkgo.When("creating a basic failure", func() {
|
||||
ginkgo.It("returns a failure with the provided message and ID", func() {
|
||||
failure := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(testMessage))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("wrapping an existing error", func() {
|
||||
ginkgo.It("combines the message and wrapped error", func() {
|
||||
failure := failures.Wrap(testMessage, testID, wrappedErr)
|
||||
expectedError := fmt.Sprintf("%s: %v", testMessage, wrappedErr)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(expectedError))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Equal(wrappedErr))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("using formatted message with arguments", func() {
|
||||
ginkgo.It("formats the message correctly", func() {
|
||||
formatMessage := "test failure %d"
|
||||
failure := failures.Wrap(formatMessage, testID, nil, 123)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal("test failure 123"))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Failure interface methods", func() {
|
||||
var failure failures.Failure
|
||||
|
||||
// Setup a failure with a wrapped error before each test
|
||||
ginkgo.BeforeEach(func() {
|
||||
failure = failures.Wrap(testMessage, testID, wrappedErr)
|
||||
})
|
||||
|
||||
ginkgo.Describe("Error method", func() {
|
||||
ginkgo.It("returns only the message when no wrapped error exists", func() {
|
||||
failureNoWrap := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failureNoWrap.Error()).To(gomega.Equal(testMessage))
|
||||
})
|
||||
ginkgo.It("combines message with wrapped error", func() {
|
||||
expected := fmt.Sprintf("%s: %v", testMessage, wrappedErr)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("ID method", func() {
|
||||
ginkgo.It("returns the assigned ID", func() {
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Unwrap method", func() {
|
||||
ginkgo.It("returns the wrapped error", func() {
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Equal(wrappedErr))
|
||||
})
|
||||
ginkgo.It("returns nil when no wrapped error exists", func() {
|
||||
failureNoWrap := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failureNoWrap.Unwrap()).To(gomega.Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Is method", func() {
|
||||
ginkgo.It("returns true for failures with the same ID", func() {
|
||||
f1 := failures.Wrap("first", testID, nil)
|
||||
f2 := failures.Wrap("second", testID, nil)
|
||||
gomega.Expect(f1.Is(f2)).To(gomega.BeTrue())
|
||||
gomega.Expect(f2.Is(f1)).To(gomega.BeTrue())
|
||||
})
|
||||
ginkgo.It("returns false for failures with different IDs", func() {
|
||||
f1 := failures.Wrap("first", testID, nil)
|
||||
f2 := failures.Wrap("second", testID+1, nil)
|
||||
gomega.Expect(f1.Is(f2)).To(gomega.BeFalse())
|
||||
gomega.Expect(f2.Is(f1)).To(gomega.BeFalse())
|
||||
})
|
||||
ginkgo.It("returns false when comparing with a non-failure error", func() {
|
||||
f1 := failures.Wrap("first", testID, nil)
|
||||
gomega.Expect(f1.Is(wrappedErr)).To(gomega.BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("edge cases", func() {
|
||||
ginkgo.When("wrapping with an empty message", func() {
|
||||
ginkgo.It("handles an empty message gracefully", func() {
|
||||
failure := failures.Wrap("", testID, wrappedErr)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(": " + wrappedErr.Error()))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Equal(wrappedErr))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("wrapping with nil error and no args", func() {
|
||||
ginkgo.It("returns a valid failure with just message and ID", func() {
|
||||
failure := failures.Wrap(testMessage, testID, nil)
|
||||
gomega.Expect(failure.Error()).To(gomega.Equal(testMessage))
|
||||
gomega.Expect(failure.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(failure.Unwrap()).To(gomega.Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.When("using multiple wrapped failures", func() {
|
||||
ginkgo.It("correctly chains and unwraps multiple errors", func() {
|
||||
innerErr := errors.New("inner error")
|
||||
middleErr := failures.Wrap("middle", testID+1, innerErr)
|
||||
outerErr := failures.Wrap("outer", testID, middleErr)
|
||||
gomega.Expect(outerErr.Error()).To(gomega.Equal("outer: middle: inner error"))
|
||||
gomega.Expect(outerErr.ID()).To(gomega.Equal(testID))
|
||||
gomega.Expect(outerErr.Unwrap()).To(gomega.Equal(middleErr))
|
||||
gomega.Expect(middleErr.Unwrap()).To(gomega.Equal(innerErr))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("integration-like scenarios", func() {
|
||||
ginkgo.It("works with standard error wrapping utilities", func() {
|
||||
innerErr := errors.New("inner error")
|
||||
failure := failures.Wrap("wrapped failure", testID, innerErr)
|
||||
gomega.Expect(errors.Is(failure, innerErr)).To(gomega.BeTrue()) // Matches wrapped error
|
||||
gomega.Expect(errors.Unwrap(failure)).To(gomega.Equal(innerErr))
|
||||
})
|
||||
|
||||
ginkgo.It("handles fmt.Errorf wrapping", func() {
|
||||
failure := failures.Wrap("failure", testID, nil)
|
||||
wrapped := fmt.Errorf("additional context: %w", failure)
|
||||
gomega.Expect(wrapped.Error()).To(gomega.Equal("additional context: failure"))
|
||||
gomega.Expect(errors.Unwrap(wrapped)).To(gomega.Equal(failure))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("testutils integration", func() {
|
||||
ginkgo.It("can use TestLogger for logging failures", func() {
|
||||
// Demonstrate compatibility with testutils logger
|
||||
failure := failures.Wrap("logged failure", testID, nil)
|
||||
logger := testutils.TestLogger()
|
||||
logger.Printf("Error occurred: %v", failure)
|
||||
// No assertion needed; ensures no panic during logging
|
||||
})
|
||||
})
|
||||
})
|
7
internal/meta/version.go
Normal file
7
internal/meta/version.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package meta
|
||||
|
||||
// Version of Shoutrrr.
|
||||
const Version = `0.6-dev`
|
||||
|
||||
// DocsVersion is prepended to documentation URLs and usually equals MAJOR.MINOR of Version.
|
||||
const DocsVersion = `dev`
|
48
internal/testutils/config.go
Normal file
48
internal/testutils/config.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// TestConfigGetInvalidQueryValue tests whether the config returns
|
||||
// an error when an invalid query value is requested.
|
||||
func TestConfigGetInvalidQueryValue(config types.ServiceConfig) {
|
||||
value, err := format.GetConfigQueryResolver(config).Get("invalid query var")
|
||||
gomega.ExpectWithOffset(1, value).To(gomega.BeEmpty())
|
||||
gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred())
|
||||
}
|
||||
|
||||
// TestConfigSetInvalidQueryValue tests whether the config returns
|
||||
// an error when a URL with an invalid query value is parsed.
|
||||
func TestConfigSetInvalidQueryValue(config types.ServiceConfig, rawInvalidURL string) {
|
||||
invalidURL, err := url.Parse(rawInvalidURL)
|
||||
gomega.ExpectWithOffset(1, err).
|
||||
ToNot(gomega.HaveOccurred(), "the test URL did not parse correctly")
|
||||
|
||||
err = config.SetURL(invalidURL)
|
||||
gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred())
|
||||
}
|
||||
|
||||
// TestConfigSetDefaultValues tests whether setting the default values
|
||||
// can be set for an empty config without any errors.
|
||||
func TestConfigSetDefaultValues(config types.ServiceConfig) {
|
||||
pkr := format.NewPropKeyResolver(config)
|
||||
gomega.ExpectWithOffset(1, pkr.SetDefaultProps(config)).To(gomega.Succeed())
|
||||
}
|
||||
|
||||
// TestConfigGetEnumsCount tests whether the config.Enums returns the expected amount of items.
|
||||
func TestConfigGetEnumsCount(config types.ServiceConfig, expectedCount int) {
|
||||
enums := config.Enums()
|
||||
gomega.ExpectWithOffset(1, enums).To(gomega.HaveLen(expectedCount))
|
||||
}
|
||||
|
||||
// TestConfigGetFieldsCount tests whether the config.QueryFields return the expected amount of fields.
|
||||
func TestConfigGetFieldsCount(config types.ServiceConfig, expectedCount int) {
|
||||
fields := format.GetConfigQueryResolver(config).QueryFields()
|
||||
gomega.ExpectWithOffset(1, fields).To(gomega.HaveLen(expectedCount))
|
||||
}
|
7
internal/testutils/eavesdropper.go
Normal file
7
internal/testutils/eavesdropper.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package testutils
|
||||
|
||||
// Eavesdropper is an interface that provides a way to get a summarized output of a connection RX and TX.
|
||||
type Eavesdropper interface {
|
||||
GetConversation(includeGreeting bool) string
|
||||
GetClientSentences() []string
|
||||
}
|
36
internal/testutils/failwriter.go
Normal file
36
internal/testutils/failwriter.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var ErrWriteLimitReached = errors.New("reached write limit")
|
||||
|
||||
type failWriter struct {
|
||||
writeLimit int
|
||||
writeCount int
|
||||
}
|
||||
|
||||
// Close is just a dummy function to implement io.Closer.
|
||||
func (fw *failWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write returns an error if the write limit has been reached.
|
||||
func (fw *failWriter) Write(data []byte) (int, error) {
|
||||
fw.writeCount++
|
||||
if fw.writeCount > fw.writeLimit {
|
||||
return 0, fmt.Errorf("%w: %d", ErrWriteLimitReached, fw.writeLimit)
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// CreateFailWriter returns a io.WriteCloser that returns an error after the amount of writes indicated by writeLimit.
|
||||
func CreateFailWriter(writeLimit int) io.WriteCloser {
|
||||
return &failWriter{
|
||||
writeLimit: writeLimit,
|
||||
}
|
||||
}
|
14
internal/testutils/iofaker.go
Normal file
14
internal/testutils/iofaker.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type ioFaker struct {
|
||||
io.ReadWriter
|
||||
}
|
||||
|
||||
// Close is just a dummy function to implement the io.Closer interface.
|
||||
func (iof ioFaker) Close() error {
|
||||
return nil
|
||||
}
|
12
internal/testutils/logging.go
Normal file
12
internal/testutils/logging.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
)
|
||||
|
||||
// TestLogger returns a log.Logger that writes to ginkgo.GinkgoWriter for use in tests.
|
||||
func TestLogger() *log.Logger {
|
||||
return log.New(ginkgo.GinkgoWriter, "[Test] ", 0)
|
||||
}
|
8
internal/testutils/mockclientservice.go
Normal file
8
internal/testutils/mockclientservice.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package testutils
|
||||
|
||||
import "net/http"
|
||||
|
||||
// MockClientService is used to allow mocking the HTTP client when testing.
|
||||
type MockClientService interface {
|
||||
GetHTTPClient() *http.Client
|
||||
}
|
25
internal/testutils/must.go
Normal file
25
internal/testutils/must.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/jarcoal/httpmock"
|
||||
"github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// URLMust creates a url.URL from the given rawURL and fails the test if it cannot be parsed.
|
||||
func URLMust(rawURL string) *url.URL {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// JSONRespondMust creates a httpmock.Responder with the given response
|
||||
// as the body, and fails the test if it cannot be created.
|
||||
func JSONRespondMust(code int, response any) httpmock.Responder {
|
||||
responder, err := httpmock.NewJsonResponder(code, response)
|
||||
gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred(), "invalid test response struct")
|
||||
|
||||
return responder
|
||||
}
|
14
internal/testutils/service.go
Normal file
14
internal/testutils/service.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
// TestServiceSetInvalidParamValue tests whether the service returns an error
|
||||
// when an invalid param key/value is passed through Send.
|
||||
func TestServiceSetInvalidParamValue(service types.Service, key string, value string) {
|
||||
err := service.Send("TestMessage", &types.Params{key: value})
|
||||
gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred())
|
||||
}
|
135
internal/testutils/testutils_test.go
Normal file
135
internal/testutils/testutils_test.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package testutils_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/internal/testutils"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
)
|
||||
|
||||
func TestTestUtils(t *testing.T) {
|
||||
gomega.RegisterFailHandler(ginkgo.Fail)
|
||||
|
||||
ginkgo.RunSpecs(t, "Shoutrrr TestUtils Suite")
|
||||
}
|
||||
|
||||
var _ = ginkgo.Describe("the testutils package", func() {
|
||||
ginkgo.When("calling function TestLogger", func() {
|
||||
ginkgo.It("should not return nil", func() {
|
||||
gomega.Expect(testutils.TestLogger()).NotTo(gomega.BeNil())
|
||||
})
|
||||
ginkgo.It(`should have the prefix "[Test] "`, func() {
|
||||
gomega.Expect(testutils.TestLogger().Prefix()).To(gomega.Equal("[Test] "))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Must helpers", func() {
|
||||
ginkgo.Describe("URLMust", func() {
|
||||
ginkgo.It("should panic when an invalid URL is passed", func() {
|
||||
failures := gomega.InterceptGomegaFailures(func() { testutils.URLMust(":") })
|
||||
gomega.Expect(failures).To(gomega.HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("JSONRespondMust", func() {
|
||||
ginkgo.It("should panic when an invalid struct is passed", func() {
|
||||
notAValidJSONSource := func() {}
|
||||
failures := gomega.InterceptGomegaFailures(
|
||||
func() { testutils.JSONRespondMust(200, notAValidJSONSource) },
|
||||
)
|
||||
gomega.Expect(failures).To(gomega.HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Config test helpers", func() {
|
||||
var config dummyConfig
|
||||
ginkgo.BeforeEach(func() {
|
||||
config = dummyConfig{}
|
||||
})
|
||||
ginkgo.Describe("TestConfigSetInvalidQueryValue", func() {
|
||||
ginkgo.It("should fail when not correctly implemented", func() {
|
||||
failures := gomega.InterceptGomegaFailures(func() {
|
||||
testutils.TestConfigSetInvalidQueryValue(&config, "mock://host?invalid=value")
|
||||
})
|
||||
gomega.Expect(failures).To(gomega.HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("TestConfigGetInvalidQueryValue", func() {
|
||||
ginkgo.It("should fail when not correctly implemented", func() {
|
||||
failures := gomega.InterceptGomegaFailures(func() {
|
||||
testutils.TestConfigGetInvalidQueryValue(&config)
|
||||
})
|
||||
gomega.Expect(failures).To(gomega.HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("TestConfigSetDefaultValues", func() {
|
||||
ginkgo.It("should fail when not correctly implemented", func() {
|
||||
failures := gomega.InterceptGomegaFailures(func() {
|
||||
testutils.TestConfigSetDefaultValues(&config)
|
||||
})
|
||||
gomega.Expect(failures).NotTo(gomega.BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("TestConfigGetEnumsCount", func() {
|
||||
ginkgo.It("should fail when not correctly implemented", func() {
|
||||
failures := gomega.InterceptGomegaFailures(func() {
|
||||
testutils.TestConfigGetEnumsCount(&config, 99)
|
||||
})
|
||||
gomega.Expect(failures).NotTo(gomega.BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("TestConfigGetFieldsCount", func() {
|
||||
ginkgo.It("should fail when not correctly implemented", func() {
|
||||
failures := gomega.InterceptGomegaFailures(func() {
|
||||
testutils.TestConfigGetFieldsCount(&config, 99)
|
||||
})
|
||||
gomega.Expect(failures).NotTo(gomega.BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Describe("Service test helpers", func() {
|
||||
var service dummyService
|
||||
ginkgo.BeforeEach(func() {
|
||||
service = dummyService{}
|
||||
})
|
||||
ginkgo.Describe("TestConfigSetInvalidQueryValue", func() {
|
||||
ginkgo.It("should fail when not correctly implemented", func() {
|
||||
failures := gomega.InterceptGomegaFailures(func() {
|
||||
testutils.TestServiceSetInvalidParamValue(&service, "invalid", "value")
|
||||
})
|
||||
gomega.Expect(failures).To(gomega.HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
type dummyConfig struct {
|
||||
standard.EnumlessConfig
|
||||
Foo uint64 `default:"-1" key:"foo"`
|
||||
}
|
||||
|
||||
func (dc *dummyConfig) GetURL() *url.URL { return &url.URL{} }
|
||||
func (dc *dummyConfig) SetURL(_ *url.URL) error { return nil }
|
||||
func (dc *dummyConfig) Get(string) (string, error) { return "", nil }
|
||||
func (dc *dummyConfig) Set(string, string) error { return nil }
|
||||
func (dc *dummyConfig) QueryFields() []string { return []string{} }
|
||||
|
||||
type dummyService struct {
|
||||
standard.Standard
|
||||
Config dummyConfig
|
||||
}
|
||||
|
||||
func (s *dummyService) Initialize(_ *url.URL, _ types.StdLogger) error { return nil }
|
||||
func (s *dummyService) Send(_ string, _ *types.Params) error { return nil }
|
||||
func (s *dummyService) GetID() string { return "dummy" }
|
106
internal/testutils/textconfaker.go
Normal file
106
internal/testutils/textconfaker.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type textConFaker struct {
|
||||
inputBuffer *bytes.Buffer
|
||||
inputWriter *bufio.Writer
|
||||
outputReader *bufio.Reader
|
||||
responses []string
|
||||
delim string
|
||||
}
|
||||
|
||||
func (tcf *textConFaker) GetInput() string {
|
||||
_ = tcf.inputWriter.Flush()
|
||||
|
||||
return tcf.inputBuffer.String()
|
||||
}
|
||||
|
||||
// GetConversation returns the input and output streams as a conversation.
|
||||
func (tcf *textConFaker) GetConversation(includeGreeting bool) string {
|
||||
conv := ""
|
||||
inSequence := false
|
||||
input := strings.Split(tcf.GetInput(), tcf.delim)
|
||||
responseIndex := 0
|
||||
|
||||
if includeGreeting {
|
||||
conv += fmt.Sprintf(" %-55s << %-50s\n", "(server greeting)", tcf.responses[0])
|
||||
responseIndex = 1
|
||||
}
|
||||
|
||||
for i, query := range input {
|
||||
if query == "." {
|
||||
inSequence = false
|
||||
}
|
||||
|
||||
resp := ""
|
||||
if len(tcf.responses) > responseIndex && !inSequence {
|
||||
resp = tcf.responses[responseIndex]
|
||||
}
|
||||
|
||||
if query == "" && resp == "" && i == len(input)-1 {
|
||||
break
|
||||
}
|
||||
|
||||
conv += fmt.Sprintf(" #%2d >> %50s << %-50s\n", i, query, resp)
|
||||
|
||||
for len(resp) > 3 && resp[3] == '-' {
|
||||
responseIndex++
|
||||
resp = tcf.responses[responseIndex]
|
||||
conv += fmt.Sprintf(" %50s << %-50s\n", " ", resp)
|
||||
}
|
||||
|
||||
if !inSequence {
|
||||
responseIndex++
|
||||
}
|
||||
|
||||
if len(resp) > 0 && resp[0] == '3' {
|
||||
inSequence = true
|
||||
}
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
// GetClientSentences returns all the input received from the client separated by the delimiter.
|
||||
func (tcf *textConFaker) GetClientSentences() []string {
|
||||
_ = tcf.inputWriter.Flush()
|
||||
|
||||
return strings.Split(tcf.inputBuffer.String(), tcf.delim)
|
||||
}
|
||||
|
||||
// CreateReadWriter returns a ReadWriter from the textConFakers internal reader and writer.
|
||||
func (tcf *textConFaker) CreateReadWriter() *bufio.ReadWriter {
|
||||
return bufio.NewReadWriter(tcf.outputReader, tcf.inputWriter)
|
||||
}
|
||||
|
||||
func (tcf *textConFaker) init() {
|
||||
tcf.inputBuffer = &bytes.Buffer{}
|
||||
stringReader := strings.NewReader(strings.Join(tcf.responses, tcf.delim))
|
||||
tcf.outputReader = bufio.NewReader(stringReader)
|
||||
tcf.inputWriter = bufio.NewWriter(tcf.inputBuffer)
|
||||
}
|
||||
|
||||
// CreateTextConFaker returns a textproto.Conn to fake textproto based connections.
|
||||
func CreateTextConFaker(responses []string, delim string) (*textproto.Conn, Eavesdropper) {
|
||||
tcfaker := textConFaker{
|
||||
responses: responses,
|
||||
delim: delim,
|
||||
}
|
||||
tcfaker.init()
|
||||
|
||||
// rx := iotest.NewReadLogger("TextConRx", tcfaker.outputReader)
|
||||
// tx := iotest.NewWriteLogger("TextConTx", tcfaker.inputWriter)
|
||||
// faker := CreateIOFaker(rx, tx)
|
||||
faker := ioFaker{
|
||||
ReadWriter: tcfaker.CreateReadWriter(),
|
||||
}
|
||||
|
||||
return textproto.NewConn(faker), &tcfaker
|
||||
}
|
37
internal/util/cobra.go
Normal file
37
internal/util/cobra.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// LoadFlagsFromAltSources is a WORKAROUND to make cobra count env vars and
|
||||
// positional arguments when checking required flags.
|
||||
func LoadFlagsFromAltSources(cmd *cobra.Command, args []string) {
|
||||
flags := cmd.Flags()
|
||||
|
||||
if len(args) > 0 {
|
||||
_ = flags.Set("url", args[0])
|
||||
|
||||
if len(args) > 1 {
|
||||
_ = flags.Set("message", args[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if hasURLInEnvButNotFlag(cmd) {
|
||||
_ = flags.Set("url", viper.GetViper().GetString("SHOUTRRR_URL"))
|
||||
|
||||
// If the URL has been set in ENV, default the message to read from stdin.
|
||||
if msg, _ := flags.GetString("message"); msg == "" {
|
||||
_ = flags.Set("message", "-")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasURLInEnvButNotFlag(cmd *cobra.Command) bool {
|
||||
s, _ := cmd.Flags().GetString("url")
|
||||
|
||||
return s == "" && viper.GetViper().GetString("SHOUTRRR_URL") != ""
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue