324 lines
8.8 KiB
Go
324 lines
8.8 KiB
Go
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)
|
|
}
|