188 lines
4.2 KiB
Go
188 lines
4.2 KiB
Go
package harness
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const mailStubPath = "/api/v1/internal/login-code-deliveries"
|
|
|
|
// LoginCodeDelivery stores one mail-delivery request received by the external
|
|
// mail stub.
|
|
type LoginCodeDelivery struct {
|
|
// Email identifies the target e-mail address requested by authsession.
|
|
Email string
|
|
|
|
// Code stores the cleartext login code requested by authsession.
|
|
Code string
|
|
|
|
// Locale stores the canonical BCP 47 language tag selected by authsession.
|
|
Locale string
|
|
}
|
|
|
|
// MailBehavior overrides one external mail-stub response.
|
|
type MailBehavior struct {
|
|
// Delay waits before the stub writes its response.
|
|
Delay time.Duration
|
|
|
|
// StatusCode overrides the HTTP status returned by the stub. Zero keeps the
|
|
// default `200 OK`.
|
|
StatusCode int
|
|
|
|
// RawBody overrides the exact response body returned by the stub. Empty
|
|
// value keeps the default JSON payload for the chosen status.
|
|
RawBody string
|
|
}
|
|
|
|
// MailStub provides one stateful external HTTP mail-service stub.
|
|
type MailStub struct {
|
|
server *httptest.Server
|
|
|
|
mu sync.Mutex
|
|
deliveries []LoginCodeDelivery
|
|
behavior MailBehavior
|
|
}
|
|
|
|
// NewMailStub starts one stateful external HTTP mail-service stub.
|
|
func NewMailStub(t testing.TB) *MailStub {
|
|
t.Helper()
|
|
|
|
stub := &MailStub{}
|
|
stub.server = httptest.NewServer(http.HandlerFunc(stub.handle))
|
|
t.Cleanup(stub.server.Close)
|
|
return stub
|
|
}
|
|
|
|
// BaseURL returns the stub base URL suitable for service runtime wiring.
|
|
func (s *MailStub) BaseURL() string {
|
|
if s == nil || s.server == nil {
|
|
return ""
|
|
}
|
|
return s.server.URL
|
|
}
|
|
|
|
// SetBehavior replaces the current response behavior used by subsequent
|
|
// requests.
|
|
func (s *MailStub) SetBehavior(behavior MailBehavior) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.behavior = behavior
|
|
}
|
|
|
|
// RecordedDeliveries returns a snapshot of all delivery requests received by
|
|
// the stub so far.
|
|
func (s *MailStub) RecordedDeliveries() []LoginCodeDelivery {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
cloned := make([]LoginCodeDelivery, len(s.deliveries))
|
|
copy(cloned, s.deliveries)
|
|
return cloned
|
|
}
|
|
|
|
// Reset clears the recorded deliveries and restores default behavior.
|
|
func (s *MailStub) Reset() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.deliveries = nil
|
|
s.behavior = MailBehavior{}
|
|
}
|
|
|
|
func (s *MailStub) handle(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Method != http.MethodPost || request.URL.Path != mailStubPath {
|
|
http.NotFound(writer, request)
|
|
return
|
|
}
|
|
|
|
var payload struct {
|
|
Email string `json:"email"`
|
|
Code string `json:"code"`
|
|
Locale string `json:"locale"`
|
|
}
|
|
if err := decodeStrictJSONRequest(request, &payload); err != nil {
|
|
http.Error(writer, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.deliveries = append(s.deliveries, LoginCodeDelivery{
|
|
Email: payload.Email,
|
|
Code: payload.Code,
|
|
Locale: payload.Locale,
|
|
})
|
|
behavior := s.behavior
|
|
s.mu.Unlock()
|
|
|
|
if behavior.Delay > 0 {
|
|
timer := time.NewTimer(behavior.Delay)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case <-request.Context().Done():
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
|
|
statusCode := behavior.StatusCode
|
|
if statusCode == 0 {
|
|
statusCode = http.StatusOK
|
|
}
|
|
|
|
body := behavior.RawBody
|
|
if body == "" {
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
body = `{"outcome":"sent"}`
|
|
default:
|
|
body = `{"error":"stubbed mail failure"}`
|
|
}
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
writer.WriteHeader(statusCode)
|
|
_, _ = io.WriteString(writer, body)
|
|
}
|
|
|
|
func decodeStrictJSONRequest(request *http.Request, target any) error {
|
|
decoder := json.NewDecoder(request.Body)
|
|
decoder.DisallowUnknownFields()
|
|
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return errors.New("unexpected trailing JSON input")
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func decodeStrictJSONPayload(payload []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return errors.New("unexpected trailing JSON input")
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|