Skip to content

Commit a064e1b

Browse files
committed
test(sink): cover http and sinktest error branches
Add the http build-request and PostJSON marshal-error paths, and exercise the sinktest conformance harness's Flusher and Shutdowner error and panic branches plus its nil-Outlet guard. Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
1 parent 3b5ea21 commit a064e1b

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

sink/http/error_paths_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package http_test
4+
5+
import (
6+
"context"
7+
"strings"
8+
"testing"
9+
10+
httpsink "github.com/stablekernel/crucible/sink/http"
11+
)
12+
13+
// TestPost_BuildRequestError exercises the NewRequestWithContext failure branch:
14+
// a URL containing a control character cannot be parsed into a request, so Post
15+
// returns a build-request error before any Do call.
16+
func TestPost_BuildRequestError(t *testing.T) {
17+
t.Parallel()
18+
19+
doer := &fakeDoer{}
20+
// A control character in the URL makes http.NewRequestWithContext fail.
21+
err := httpsink.Post("http://example.test/\x7f", "application/json", []byte("{}")).
22+
Apply(context.Background(), doer)
23+
if err == nil {
24+
t.Fatal("Apply() = nil, want a build-request error")
25+
}
26+
if !strings.Contains(err.Error(), "build request") {
27+
t.Errorf("error = %v, want it to mention building the request", err)
28+
}
29+
if len(doer.requests) != 0 {
30+
t.Errorf("Do() called %d times, want 0 (request never built)", len(doer.requests))
31+
}
32+
}
33+
34+
// unmarshalable is a type json.Marshal rejects: a channel field cannot be
35+
// encoded, driving the PostJSON marshal-error branch.
36+
type unmarshalable struct {
37+
Ch chan int
38+
}
39+
40+
// TestPostJSON_MarshalError exercises the json.Marshal failure branch: an
41+
// unmarshalable payload returns a marshal error before any request is built.
42+
func TestPostJSON_MarshalError(t *testing.T) {
43+
t.Parallel()
44+
45+
doer := &fakeDoer{}
46+
err := httpsink.PostJSON("http://example.test/items", unmarshalable{Ch: make(chan int)}).
47+
Apply(context.Background(), doer)
48+
if err == nil {
49+
t.Fatal("Apply() = nil, want a marshal error")
50+
}
51+
if !strings.Contains(err.Error(), "marshal json") {
52+
t.Errorf("error = %v, want it to mention marshaling json", err)
53+
}
54+
if len(doer.requests) != 0 {
55+
t.Errorf("Do() called %d times, want 0 (marshal failed first)", len(doer.requests))
56+
}
57+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package sinktest
4+
5+
import (
6+
"context"
7+
"errors"
8+
"testing"
9+
10+
"github.com/stablekernel/crucible/sink"
11+
)
12+
13+
// flushOutlet is a conforming Outlet whose Flush behavior is configurable, so
14+
// the harness's Flusher branch can be driven into both its error and panic
15+
// arms.
16+
type flushOutlet struct {
17+
flushErr error
18+
flushPanic bool
19+
}
20+
21+
func (o *flushOutlet) Sink(_ context.Context, _ any) error { return sink.ErrUnregistered }
22+
23+
func (o *flushOutlet) Flush(context.Context) error {
24+
if o.flushPanic {
25+
panic("flush boom")
26+
}
27+
return o.flushErr
28+
}
29+
30+
// shutdownOutlet is a conforming Outlet whose Shutdown behavior is configurable.
31+
type shutdownOutlet struct {
32+
shutdownErr error
33+
shutdownPanic bool
34+
}
35+
36+
func (o *shutdownOutlet) Sink(_ context.Context, _ any) error { return sink.ErrUnregistered }
37+
38+
func (o *shutdownOutlet) Shutdown(context.Context) error {
39+
if o.shutdownPanic {
40+
panic("shutdown boom")
41+
}
42+
return o.shutdownErr
43+
}
44+
45+
func TestCheckFlusherReportsError(t *testing.T) {
46+
t.Parallel()
47+
48+
boom := errors.New("dirty flush")
49+
errs := checkOutlet(func() sink.Outlet { return &flushOutlet{flushErr: boom} })
50+
if !containsErr(errs, "Flush on a clean outlet") {
51+
t.Fatalf("checkOutlet did not flag a flush error; got %v", errs)
52+
}
53+
}
54+
55+
func TestCheckFlusherRecoversPanic(t *testing.T) {
56+
t.Parallel()
57+
58+
errs := checkOutlet(func() sink.Outlet { return &flushOutlet{flushPanic: true} })
59+
if !containsErr(errs, "Flush panicked") {
60+
t.Fatalf("checkOutlet did not report a flush panic; got %v", errs)
61+
}
62+
}
63+
64+
func TestCheckShutdownerReportsError(t *testing.T) {
65+
t.Parallel()
66+
67+
boom := errors.New("dirty shutdown")
68+
errs := checkOutlet(func() sink.Outlet { return &shutdownOutlet{shutdownErr: boom} })
69+
if !containsErr(errs, "Shutdown returned") {
70+
t.Fatalf("checkOutlet did not flag a shutdown error; got %v", errs)
71+
}
72+
}
73+
74+
func TestCheckShutdownerRecoversPanic(t *testing.T) {
75+
t.Parallel()
76+
77+
errs := checkOutlet(func() sink.Outlet { return &shutdownOutlet{shutdownPanic: true} })
78+
if !containsErr(errs, "Shutdown panicked") {
79+
t.Fatalf("checkOutlet did not report a shutdown panic; got %v", errs)
80+
}
81+
}
82+
83+
func TestCheckOutletRejectsNilOutlet(t *testing.T) {
84+
t.Parallel()
85+
86+
errs := checkOutlet(func() sink.Outlet { return nil })
87+
if !containsErr(errs, "nil Outlet") {
88+
t.Fatalf("checkOutlet did not flag a nil Outlet; got %v", errs)
89+
}
90+
}
91+
92+
func containsErr(errs []error, substr string) bool {
93+
for _, e := range errs {
94+
if e != nil && contains(e.Error(), substr) {
95+
return true
96+
}
97+
}
98+
return false
99+
}
100+
101+
func contains(haystack, needle string) bool {
102+
for i := 0; i+len(needle) <= len(haystack); i++ {
103+
if haystack[i:i+len(needle)] == needle {
104+
return true
105+
}
106+
}
107+
return false
108+
}

0 commit comments

Comments
 (0)