tests: integration suite
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
package harness
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
resolveByEmailPath = "/api/v1/internal/user-resolutions/by-email"
|
||||
ensureByEmailPath = "/api/v1/internal/users/ensure-by-email"
|
||||
blockByEmailPath = "/api/v1/internal/user-blocks/by-email"
|
||||
)
|
||||
|
||||
// EnsureUserCall stores one ensure-by-email request received by the external
|
||||
// user-service stub.
|
||||
type EnsureUserCall struct {
|
||||
// Email identifies the requested login or registration e-mail.
|
||||
Email string
|
||||
|
||||
// PreferredLanguage stores the forwarded registration-context language.
|
||||
PreferredLanguage string
|
||||
|
||||
// TimeZone stores the forwarded registration-context time zone.
|
||||
TimeZone string
|
||||
}
|
||||
|
||||
// UserStub provides one stateful external HTTP user-service stub.
|
||||
type UserStub struct {
|
||||
server *httptest.Server
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
emailToUserID map[string]string
|
||||
userIDToEmail map[string]string
|
||||
blockedEmails map[string]string
|
||||
blockedUsers map[string]string
|
||||
ensureCalls []EnsureUserCall
|
||||
nextUserID int
|
||||
}
|
||||
|
||||
// NewUserStub starts one stateful external HTTP user-service stub.
|
||||
func NewUserStub(t testing.TB) *UserStub {
|
||||
t.Helper()
|
||||
|
||||
stub := &UserStub{
|
||||
emailToUserID: make(map[string]string),
|
||||
userIDToEmail: make(map[string]string),
|
||||
blockedEmails: make(map[string]string),
|
||||
blockedUsers: make(map[string]string),
|
||||
nextUserID: 1,
|
||||
}
|
||||
stub.server = httptest.NewServer(http.HandlerFunc(stub.handle))
|
||||
t.Cleanup(stub.server.Close)
|
||||
return stub
|
||||
}
|
||||
|
||||
// BaseURL returns the stub base URL suitable for authsession runtime wiring.
|
||||
func (s *UserStub) BaseURL() string {
|
||||
if s == nil || s.server == nil {
|
||||
return ""
|
||||
}
|
||||
return s.server.URL
|
||||
}
|
||||
|
||||
// SeedExisting adds one existing unblocked user record into the stub state.
|
||||
func (s *UserStub) SeedExisting(email string, userID string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.emailToUserID[email] = userID
|
||||
s.userIDToEmail[userID] = email
|
||||
}
|
||||
|
||||
// SeedBlockedEmail adds one blocked e-mail into the stub state.
|
||||
func (s *UserStub) SeedBlockedEmail(email string, reasonCode string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.blockedEmails[email] = reasonCode
|
||||
if userID, ok := s.emailToUserID[email]; ok {
|
||||
s.blockedUsers[userID] = reasonCode
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureCalls returns a snapshot of ensure-by-email requests observed by the
|
||||
// stub so far.
|
||||
func (s *UserStub) EnsureCalls() []EnsureUserCall {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
cloned := make([]EnsureUserCall, len(s.ensureCalls))
|
||||
copy(cloned, s.ensureCalls)
|
||||
return cloned
|
||||
}
|
||||
|
||||
// Reset clears all stub state and recorded calls.
|
||||
func (s *UserStub) Reset() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.emailToUserID = make(map[string]string)
|
||||
s.userIDToEmail = make(map[string]string)
|
||||
s.blockedEmails = make(map[string]string)
|
||||
s.blockedUsers = make(map[string]string)
|
||||
s.ensureCalls = nil
|
||||
s.nextUserID = 1
|
||||
}
|
||||
|
||||
func (s *UserStub) handle(writer http.ResponseWriter, request *http.Request) {
|
||||
switch {
|
||||
case request.Method == http.MethodPost && request.URL.Path == resolveByEmailPath:
|
||||
s.handleResolveByEmail(writer, request)
|
||||
case request.Method == http.MethodGet && strings.HasPrefix(request.URL.Path, "/api/v1/internal/users/") && strings.HasSuffix(request.URL.Path, "/exists"):
|
||||
s.handleExistsByUserID(writer, request)
|
||||
case request.Method == http.MethodPost && request.URL.Path == ensureByEmailPath:
|
||||
s.handleEnsureByEmail(writer, request)
|
||||
case request.Method == http.MethodPost && strings.HasPrefix(request.URL.Path, "/api/v1/internal/users/") && strings.HasSuffix(request.URL.Path, "/block"):
|
||||
s.handleBlockByUserID(writer, request)
|
||||
case request.Method == http.MethodPost && request.URL.Path == blockByEmailPath:
|
||||
s.handleBlockByEmail(writer, request)
|
||||
default:
|
||||
http.NotFound(writer, request)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserStub) handleResolveByEmail(writer http.ResponseWriter, request *http.Request) {
|
||||
var payload struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := decodeStrictJSONRequest(request, &payload); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if reason, ok := s.blockedEmails[payload.Email]; ok {
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"kind": "blocked",
|
||||
"block_reason_code": reason,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if userID, ok := s.emailToUserID[payload.Email]; ok {
|
||||
if reason, blocked := s.blockedUsers[userID]; blocked {
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"kind": "blocked",
|
||||
"block_reason_code": reason,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"kind": "existing",
|
||||
"user_id": userID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, map[string]any{"kind": "creatable"})
|
||||
}
|
||||
|
||||
func (s *UserStub) handleExistsByUserID(writer http.ResponseWriter, request *http.Request) {
|
||||
userIDValue := strings.TrimSuffix(strings.TrimPrefix(request.URL.Path, "/api/v1/internal/users/"), "/exists")
|
||||
userIDValue, err := url.PathUnescape(userIDValue)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
_, exists := s.userIDToEmail[userIDValue]
|
||||
writeJSON(writer, http.StatusOK, map[string]bool{"exists": exists})
|
||||
}
|
||||
|
||||
func (s *UserStub) handleEnsureByEmail(writer http.ResponseWriter, request *http.Request) {
|
||||
var payload struct {
|
||||
Email string `json:"email"`
|
||||
RegistrationContext *struct {
|
||||
PreferredLanguage string `json:"preferred_language"`
|
||||
TimeZone string `json:"time_zone"`
|
||||
} `json:"registration_context"`
|
||||
}
|
||||
if err := decodeStrictJSONRequest(request, &payload); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if payload.RegistrationContext == nil {
|
||||
http.Error(writer, "registration_context must be present", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.ensureCalls = append(s.ensureCalls, EnsureUserCall{
|
||||
Email: payload.Email,
|
||||
PreferredLanguage: payload.RegistrationContext.PreferredLanguage,
|
||||
TimeZone: payload.RegistrationContext.TimeZone,
|
||||
})
|
||||
|
||||
if reason, ok := s.blockedEmails[payload.Email]; ok {
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"outcome": "blocked",
|
||||
"block_reason_code": reason,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if userID, ok := s.emailToUserID[payload.Email]; ok {
|
||||
if reason, blocked := s.blockedUsers[userID]; blocked {
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"outcome": "blocked",
|
||||
"block_reason_code": reason,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"outcome": "existing",
|
||||
"user_id": userID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userID := fmt.Sprintf("user-%d", s.nextUserID)
|
||||
s.nextUserID++
|
||||
s.emailToUserID[payload.Email] = userID
|
||||
s.userIDToEmail[userID] = payload.Email
|
||||
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"outcome": "created",
|
||||
"user_id": userID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserStub) handleBlockByUserID(writer http.ResponseWriter, request *http.Request) {
|
||||
userIDValue := strings.TrimSuffix(strings.TrimPrefix(request.URL.Path, "/api/v1/internal/users/"), "/block")
|
||||
userIDValue, err := url.PathUnescape(userIDValue)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
ReasonCode string `json:"reason_code"`
|
||||
}
|
||||
if err := decodeStrictJSONRequest(request, &payload); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
email, exists := s.userIDToEmail[userIDValue]
|
||||
if !exists {
|
||||
writeJSON(writer, http.StatusNotFound, map[string]string{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
outcome := "blocked"
|
||||
if _, already := s.blockedUsers[userIDValue]; already {
|
||||
outcome = "already_blocked"
|
||||
}
|
||||
s.blockedUsers[userIDValue] = payload.ReasonCode
|
||||
s.blockedEmails[email] = payload.ReasonCode
|
||||
|
||||
writeJSON(writer, http.StatusOK, map[string]any{
|
||||
"outcome": outcome,
|
||||
"user_id": userIDValue,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserStub) handleBlockByEmail(writer http.ResponseWriter, request *http.Request) {
|
||||
var payload struct {
|
||||
Email string `json:"email"`
|
||||
ReasonCode string `json:"reason_code"`
|
||||
}
|
||||
if err := decodeStrictJSONRequest(request, &payload); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
outcome := "blocked"
|
||||
if _, already := s.blockedEmails[payload.Email]; already {
|
||||
outcome = "already_blocked"
|
||||
}
|
||||
s.blockedEmails[payload.Email] = payload.ReasonCode
|
||||
|
||||
response := map[string]any{"outcome": outcome}
|
||||
if userID, ok := s.emailToUserID[payload.Email]; ok {
|
||||
s.blockedUsers[userID] = payload.ReasonCode
|
||||
response["user_id"] = userID
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func writeJSON(writer http.ResponseWriter, statusCode int, value any) {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(statusCode)
|
||||
_, _ = writer.Write(payload)
|
||||
}
|
||||
Reference in New Issue
Block a user