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,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))
}

View 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
}

View 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,
}
}

View 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
}

View 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)
}

View 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
}

View 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
}

View 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())
}

View 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" }

View 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
}