Skip to content

Commit 2f54c7f

Browse files
committed
feat: add Responder.Then() method
Signed-off-by: Maxime Soulé <btik-git@scoubidou.com>
1 parent 25b82c6 commit 2f54c7f

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

response.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package httpmock
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"encoding/xml"
8+
"errors"
79
"fmt"
810
"io"
911
"net/http"
12+
"reflect"
1013
"strconv"
1114
"strings"
1215
"sync"
@@ -122,6 +125,79 @@ func (r Responder) Delay(d time.Duration) Responder {
122125
}
123126
}
124127

128+
type fromThenKeyType struct{}
129+
130+
var fromThenKey = fromThenKeyType{}
131+
132+
var errThenDone = errors.New("ThenDone")
133+
134+
// similar is simple but a bit tricky. Here we consider two Responder
135+
// are similar if they share the same function, but not necessarily
136+
// the same environment. It is only used by Then below.
137+
func (r Responder) similar(other Responder) bool {
138+
return reflect.ValueOf(r).Pointer() == reflect.ValueOf(other).Pointer()
139+
}
140+
141+
// Then returns a new Responder that calls r on first invocation, then
142+
// next on following ones, except when Then is chained, in this case
143+
// next is called only once:
144+
// A := httpmock.NewStringResponder(200, "A")
145+
// B := httpmock.NewStringResponder(200, "B")
146+
// C := httpmock.NewStringResponder(200, "C")
147+
//
148+
// httpmock.RegisterResponder("GET", "/pipo", A.Then(B).Then(C))
149+
//
150+
// http.Get("http://foo.bar/pipo") // A is called
151+
// http.Get("http://foo.bar/pipo") // B is called
152+
// http.Get("http://foo.bar/pipo") // C is called
153+
// http.Get("http://foo.bar/pipo") // C is called, and so on
154+
//
155+
// A panic occurs if next is the result of another Then call (because
156+
// allowing it could cause inextricable problems at runtime). Then
157+
// calls can be chained, but cannot call each other by
158+
// parameter. Example:
159+
// A.Then(B).Then(C) // is OK
160+
// A.Then(B.Then(C)) // panics as A.Then() parameter is another Then() call
161+
func (r Responder) Then(next Responder) (x Responder) {
162+
var done int
163+
var mu sync.Mutex
164+
x = func(req *http.Request) (*http.Response, error) {
165+
mu.Lock()
166+
defer mu.Unlock()
167+
168+
ctx := req.Context()
169+
thenCalledUs, _ := ctx.Value(fromThenKey).(bool)
170+
if !thenCalledUs {
171+
req = req.WithContext(context.WithValue(ctx, fromThenKey, true))
172+
}
173+
174+
switch done {
175+
case 0:
176+
resp, err := r(req)
177+
if err != errThenDone {
178+
if !x.similar(r) { // r is NOT a Then
179+
done = 1
180+
}
181+
return resp, err
182+
}
183+
fallthrough
184+
185+
case 1:
186+
done = 2 // next is NEVER a Then, as it is forbidden by design
187+
return next(req)
188+
}
189+
if thenCalledUs {
190+
return nil, errThenDone
191+
}
192+
return next(req)
193+
}
194+
195+
if next.similar(x) {
196+
panic("Then() does not accept another Then() Responder as parameter")
197+
}
198+
return
199+
}
200+
125201
// ResponderFromResponse wraps an *http.Response in a Responder.
126202
//
127203
// Be careful, except for responses generated by httpmock

response_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,98 @@ func TestResponder(t *testing.T) {
655655
}
656656
}
657657

658+
func TestResponder_Then(t *testing.T) {
659+
req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil)
660+
if err != nil {
661+
t.Fatalf("Error creating request: %s", err)
662+
}
663+
664+
//
665+
// Then
666+
var stack string
667+
newResponder := func(level string) Responder {
668+
return func(*http.Request) (*http.Response, error) {
669+
stack += level
670+
return NewStringResponse(200, level), nil
671+
}
672+
}
673+
var rt Responder
674+
chk := func(t *testing.T, expectedLevel, expectedStack string) {
675+
helper(t).Helper()
676+
resp, err := rt(req)
677+
if err != nil {
678+
t.Errorf("Responder retruned an unexpected error: %s", err)
679+
return
680+
}
681+
b, err := ioutil.ReadAll(resp.Body)
682+
if err != nil {
683+
t.Errorf("Read response failed: %s", err)
684+
return
685+
}
686+
if string(b) != expectedLevel {
687+
t.Errorf("level: got %q but expected %q (stack is %q)",
688+
b, expectedLevel, stack)
689+
return
690+
}
691+
if stack != expectedStack {
692+
t.Errorf("stack: got %q but expected %q", stack, expectedStack)
693+
return
694+
}
695+
}
696+
697+
A, B, C := newResponder("A"), newResponder("B"), newResponder("C")
698+
D, E, F := newResponder("D"), newResponder("E"), newResponder("F")
699+
700+
t.Run("simple", func(t *testing.T) {
701+
// (r=A,then=B)
702+
rt = A.Then(B)
703+
704+
chk(t, "A", "A")
705+
chk(t, "B", "AB")
706+
chk(t, "B", "ABB")
707+
chk(t, "B", "ABBB")
708+
})
709+
710+
stack = ""
711+
712+
t.Run("simple chained", func(t *testing.T) {
713+
// (r=A,then=B)
714+
// (r=↑,then=C)
715+
// (r=↑,then=D)
716+
// (r=↑,then=E)
717+
// (r=↑,then=F)
718+
rt = A.Then(B).
719+
Then(C).
720+
Then(D).
721+
Then(E).
722+
Then(F)
723+
724+
chk(t, "A", "A")
725+
chk(t, "B", "AB")
726+
chk(t, "C", "ABC")
727+
chk(t, "D", "ABCD")
728+
chk(t, "E", "ABCDE")
729+
chk(t, "F", "ABCDEF")
730+
chk(t, "F", "ABCDEFF")
731+
chk(t, "F", "ABCDEFFF")
732+
})
733+
734+
stack = ""
735+
736+
t.Run("Then Responder as Then param", func(t *testing.T) {
737+
panicked, str := catchPanic(func() {
738+
A.Then(B.Then(C))
739+
})
740+
if str != "Then() does not accept another Then() Responder as parameter" {
741+
if !panicked {
742+
t.Error("Should have panicked")
743+
} else {
744+
t.Errorf("Wrong panic message: %q", str)
745+
}
746+
}
747+
})
748+
}
749+
658750
func TestParallelResponder(t *testing.T) {
659751
req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil)
660752
if err != nil {

0 commit comments

Comments
 (0)