1
0
Fork 0
golang-github-nicholas-fedo.../pkg/util/jsonclient/jsonclient_test.go
Daniel Baumann c0c4addb85
Adding upstream version 0.8.9.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-22 10:16:14 +02:00

334 lines
12 KiB
Go

package jsonclient_test
import (
"errors"
"net"
"net/http"
"testing"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp"
"github.com/nicholas-fedor/shoutrrr/pkg/util/jsonclient"
)
func TestJSONClient(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "JSONClient Suite")
}
var _ = ginkgo.Describe("JSONClient", func() {
var server *ghttp.Server
var client jsonclient.Client
ginkgo.BeforeEach(func() {
server = ghttp.NewServer()
client = jsonclient.NewClient()
})
ginkgo.When("the server returns an invalid JSON response", func() {
ginkgo.It("should return an error", func() {
server.AppendHandlers(ghttp.RespondWith(http.StatusOK, "invalid json"))
res := &mockResponse{}
err := client.Get(server.URL(), res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).
To(gomega.MatchError("invalid character 'i' looking for beginning of value"))
gomega.Expect(res.Status).To(gomega.BeEmpty())
})
})
ginkgo.When("the server returns an empty response", func() {
ginkgo.It("should return an error", func() {
server.AppendHandlers(ghttp.RespondWith(http.StatusOK, nil))
res := &mockResponse{}
err := client.Get(server.URL(), res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).To(gomega.MatchError("unexpected end of JSON input"))
gomega.Expect(res.Status).To(gomega.BeEmpty())
})
})
ginkgo.It("should deserialize GET response", func() {
server.AppendHandlers(
ghttp.RespondWithJSONEncoded(http.StatusOK, mockResponse{Status: "OK"}),
)
res := &mockResponse{}
err := client.Get(server.URL(), res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(res.Status).To(gomega.Equal("OK"))
})
ginkgo.Describe("Top-level Functions", func() {
ginkgo.It("should handle GET via DefaultClient", func() {
server.AppendHandlers(
ghttp.RespondWithJSONEncoded(http.StatusOK, mockResponse{Status: "Default OK"}),
)
res := &mockResponse{}
err := jsonclient.Get(server.URL(), res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(res.Status).To(gomega.Equal("Default OK"))
})
ginkgo.It("should handle POST via DefaultClient", func() {
req := &mockRequest{Number: 10}
res := &mockResponse{}
server.AppendHandlers(ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/"),
ghttp.VerifyJSONRepresenting(&req),
ghttp.RespondWithJSONEncoded(http.StatusOK, mockResponse{Status: "Default POST"})),
)
err := jsonclient.Post(server.URL(), req, res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(res.Status).To(gomega.Equal("Default POST"))
})
})
ginkgo.Describe("POST", func() {
ginkgo.It("should de-/serialize request and response", func() {
req := &mockRequest{Number: 5}
res := &mockResponse{}
server.AppendHandlers(ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/"),
ghttp.VerifyJSONRepresenting(&req),
ghttp.RespondWithJSONEncoded(
http.StatusOK,
&mockResponse{Status: "That's Numberwang!"},
),
))
err := client.Post(server.URL(), req, res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(res.Status).To(gomega.Equal("That's Numberwang!"))
})
ginkgo.It("should return error on error status responses", func() {
server.AppendHandlers(ghttp.RespondWith(http.StatusNotFound, "Not found!"))
err := client.Post(server.URL(), &mockRequest{}, &mockResponse{})
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).To(gomega.MatchError("got unexpected HTTP status: 404 Not Found"))
})
ginkgo.It("should return error on invalid request", func() {
server.AppendHandlers(ghttp.VerifyRequest("POST", "/"))
err := client.Post(server.URL(), func() {}, &mockResponse{})
gomega.Expect(server.ReceivedRequests()).Should(gomega.BeEmpty())
gomega.Expect(err).
To(gomega.MatchError("marshaling request to JSON: json: unsupported type: func()"))
})
ginkgo.It("should return error on invalid response type", func() {
res := &mockResponse{Status: "cool skirt"}
server.AppendHandlers(ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/"),
ghttp.RespondWithJSONEncoded(http.StatusOK, res)),
)
err := client.Post(server.URL(), nil, &[]bool{})
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).
To(gomega.MatchError("json: cannot unmarshal object into Go value of type []bool"))
gomega.Expect(jsonclient.ErrorBody(err)).To(gomega.MatchJSON(`{"Status":"cool skirt"}`))
})
ginkgo.It("should handle string request without marshaling", func() {
rawJSON := `{"Number": 42}`
res := &mockResponse{}
server.AppendHandlers(ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/"),
ghttp.VerifyBody([]byte(rawJSON)),
ghttp.RespondWithJSONEncoded(http.StatusOK, mockResponse{Status: "String Worked"})),
)
err := client.Post(server.URL(), rawJSON, res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(res.Status).To(gomega.Equal("String Worked"))
})
ginkgo.It("should return error when NewRequest fails", func() {
err := client.Post("://invalid-url", &mockRequest{}, &mockResponse{})
gomega.Expect(err).To(gomega.HaveOccurred())
gomega.Expect(err.Error()).
To(gomega.ContainSubstring("creating POST request for \"://invalid-url\": parse \"://invalid-url\": missing protocol scheme"))
})
ginkgo.It("should return error when http.Client.Do fails", func() {
brokenClient := jsonclient.NewWithHTTPClient(&http.Client{
Transport: &http.Transport{
Dial: func(_, _ string) (net.Conn, error) {
return nil, errors.New("forced network error")
},
},
})
err := brokenClient.Post(server.URL(), &mockRequest{}, &mockResponse{})
gomega.Expect(err).To(gomega.HaveOccurred())
gomega.Expect(err.Error()).
To(gomega.ContainSubstring("sending POST request to \"" + server.URL() + "\": Post \"" + server.URL() + "\": forced network error"))
})
ginkgo.It("should set multiple custom headers in request", func() {
customClient := jsonclient.NewWithHTTPClient(&http.Client{})
headers := customClient.Headers()
headers.Set("X-Custom-Header", "CustomValue")
headers.Set("X-Another-Header", "AnotherValue")
req := &mockRequest{Number: 99}
res := &mockResponse{}
server.AppendHandlers(ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/"),
ghttp.VerifyHeader(http.Header{
"Content-Type": []string{jsonclient.ContentType},
"X-Custom-Header": []string{"CustomValue"},
"X-Another-Header": []string{"AnotherValue"},
}),
ghttp.VerifyJSONRepresenting(&req),
ghttp.RespondWithJSONEncoded(
http.StatusOK,
mockResponse{Status: "Headers Worked"},
),
))
err := customClient.Post(server.URL(), req, res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(res.Status).To(gomega.Equal("Headers Worked"))
})
})
ginkgo.Describe("Headers", func() {
ginkgo.It("should return default headers with Content-Type", func() {
headers := client.Headers()
gomega.Expect(headers.Get("Content-Type")).To(gomega.Equal(jsonclient.ContentType))
})
})
ginkgo.Describe("ErrorResponse", func() {
ginkgo.It("should return false for non-jsonclient.Error", func() {
res := &mockResponse{}
result := client.ErrorResponse(errors.New("generic error"), res)
gomega.Expect(result).To(gomega.BeFalse())
gomega.Expect(res.Status).To(gomega.BeEmpty())
})
ginkgo.It("should populate response from jsonclient.Error body", func() {
res := &mockResponse{}
jsonErr := jsonclient.Error{
StatusCode: http.StatusBadRequest,
Body: `{"Status": "Bad Request"}`,
}
result := client.ErrorResponse(jsonErr, res)
gomega.Expect(result).To(gomega.BeTrue())
gomega.Expect(res.Status).To(gomega.Equal("Bad Request"))
})
ginkgo.It("should return false for invalid JSON in error body", func() {
res := &mockResponse{}
jsonErr := jsonclient.Error{
StatusCode: http.StatusBadRequest,
Body: "not json",
}
result := client.ErrorResponse(jsonErr, res)
gomega.Expect(result).To(gomega.BeFalse())
gomega.Expect(res.Status).To(gomega.BeEmpty())
})
})
ginkgo.Describe("Edge Cases", func() {
ginkgo.It("should handle network failure in Get", func() {
res := &mockResponse{}
err := client.Get("http://127.0.0.1:54321", res)
gomega.Expect(err).To(gomega.HaveOccurred())
gomega.Expect(err.Error()).To(gomega.ContainSubstring("dial tcp"))
gomega.Expect(res.Status).To(gomega.BeEmpty())
})
ginkgo.It("should handle invalid JSON with success status", func() {
server.AppendHandlers(ghttp.RespondWith(http.StatusOK, "bad json"))
res := &mockResponse{}
err := client.Get(server.URL(), res)
gomega.Expect(server.ReceivedRequests()).Should(gomega.HaveLen(1))
gomega.Expect(err).
To(gomega.MatchError("invalid character 'b' looking for beginning of value"))
gomega.Expect(res.Status).To(gomega.BeEmpty())
})
ginkgo.It("should handle nil body in error response", func() {
brokenClient := jsonclient.NewWithHTTPClient(&http.Client{
Transport: &mockTransport{
response: &http.Response{
StatusCode: http.StatusBadRequest,
Status: "400 Bad Request",
Body: &failingReader{},
Header: make(http.Header),
},
},
})
res := &mockResponse{}
err := brokenClient.Get(server.URL(), res)
gomega.Expect(err).To(gomega.HaveOccurred())
gomega.Expect(err.Error()).
To(gomega.ContainSubstring("got unexpected HTTP status: 400 Bad Request"))
gomega.Expect(jsonclient.ErrorBody(err)).To(gomega.Equal(""))
})
})
ginkgo.AfterEach(func() {
server.Close()
})
})
var _ = ginkgo.Describe("Error", func() {
ginkgo.When("no internal error has been set", func() {
ginkgo.It("should return a generic message with status code", func() {
errorWithNoError := jsonclient.Error{StatusCode: http.StatusEarlyHints}
gomega.Expect(errorWithNoError.String()).To(gomega.Equal("unknown error (HTTP 103)"))
})
})
ginkgo.Describe("ErrorBody", func() {
ginkgo.When("passed a non-json error", func() {
ginkgo.It("should return an empty string", func() {
gomega.Expect(jsonclient.ErrorBody(errors.New("unrelated error"))).
To(gomega.BeEmpty())
})
})
ginkgo.When("passed a jsonclient.Error", func() {
ginkgo.It("should return the request body from that error", func() {
errorBody := `{"error": "bad user"}`
jsonErr := jsonclient.Error{Body: errorBody}
gomega.Expect(jsonclient.ErrorBody(jsonErr)).To(gomega.MatchJSON(errorBody))
})
})
})
})
type mockResponse struct {
Status string
}
type mockRequest struct {
Number int
}
// mockTransport returns a predefined response.
type mockTransport struct {
response *http.Response
}
func (mt *mockTransport) RoundTrip(*http.Request) (*http.Response, error) {
return mt.response, nil
}
// failingReader simulates an io.Reader that fails on Read.
type failingReader struct{}
func (fr *failingReader) Read([]byte) (int, error) {
return 0, errors.New("simulated read failure")
}
func (fr *failingReader) Close() error {
return nil
}