tests: integration suite

This commit is contained in:
IliaDenisov
2026-04-09 15:27:14 +02:00
parent e04fc663f0
commit 1c8e0ca48e
20 changed files with 2748 additions and 10 deletions
+323
View File
@@ -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)
}