Files
cmroubao/backend-api/internal/platform/shunyunbao/session_test.go
T

195 lines
6.4 KiB
Go

package shunyunbao
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"cmroubao/backend-api/internal/domain"
)
func TestSessionManagerCaptchaLoginAndValidationShareCookieJar(t *testing.T) {
var captchaCalls, loginCalls, userCalls int
server := httptest.NewServer(http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
switch request.URL.Path {
case CaptchaPath:
captchaCalls++
assertERPHeaders(t, request)
http.SetCookie(writer, &http.Cookie{Name: "captcha", Value: "ready", Path: "/"})
writer.Header().Set("Content-Type", "image/png")
_, _ = writer.Write([]byte("sanitized-captcha-image"))
case LoginPath:
loginCalls++
assertERPHeaders(t, request)
if cookie, err := request.Cookie("captcha"); err != nil || cookie.Value != "ready" {
t.Fatalf("login captcha cookie = %v / %v", cookie, err)
}
content, _ := io.ReadAll(request.Body)
if string(content) != `{"code":"1234","password":"test-password","username":"test-user"}` {
t.Fatalf("login body = %s", content)
}
http.SetCookie(writer, &http.Cookie{Name: "authenticated", Value: "yes", Path: "/"})
_, _ = writer.Write([]byte(`{"status":true,"data":{"user":{"id":12},"token":"never-exposed"}}`))
case UserPath:
userCalls++
if cookie, err := request.Cookie("authenticated"); err != nil || cookie.Value != "yes" {
t.Fatalf("user cookie = %v / %v", cookie, err)
}
_, _ = writer.Write([]byte(`{"status":true,"data":{"id":12}}`))
default:
writer.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
manager := testSessionManager(t, server.URL, "test-user", "test-password")
status, err := manager.FetchCaptcha(context.Background())
if err != nil || !status.Configured || !status.CaptchaReady || status.CaptchaTicket == "" {
t.Fatalf("FetchCaptcha() = %+v, %v", status, err)
}
image, err := manager.OpenCaptcha(status.CaptchaTicket)
if err != nil || image.ContentType != "image/png" ||
string(image.Content) != "sanitized-captcha-image" {
t.Fatalf("OpenCaptcha() = %+v, %v", image, err)
}
status, err = manager.Login(context.Background(), status.CaptchaTicket, "1234")
if err != nil || !status.Authenticated || status.CaptchaReady || status.CaptchaTicket != "" {
t.Fatalf("Login() = %+v, %v", status, err)
}
status, err = manager.Validate(context.Background())
if err != nil || !status.Authenticated || captchaCalls != 1 || loginCalls != 1 || userCalls != 2 {
t.Fatalf("Validate()/calls = %+v, %v / %d %d %d", status, err, captchaCalls, loginCalls, userCalls)
}
}
func TestSessionManagerMapsAnonymousFailuresAndExpiresState(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
switch request.URL.Path {
case CaptchaPath:
writer.Header().Set("Content-Type", "image/jpeg")
_, _ = writer.Write([]byte("captcha"))
case LoginPath:
_, _ = writer.Write([]byte(`{"status":false,"msg":"private rejected response","data":null}`))
case UserPath:
writer.WriteHeader(http.StatusUnauthorized)
_, _ = writer.Write([]byte("private session response"))
}
}))
defer server.Close()
missing := testSessionManager(t, server.URL, "", "")
if _, err := missing.FetchCaptcha(context.Background()); !errors.Is(err, domain.ErrFreightSourceNotConfigured) {
t.Fatalf("missing FetchCaptcha() error = %v", err)
}
manager := testSessionManager(t, server.URL, "test-user", "test-password")
status, err := manager.FetchCaptcha(context.Background())
if err != nil {
t.Fatalf("FetchCaptcha() error = %v", err)
}
_, err = manager.Login(context.Background(), status.CaptchaTicket, "bad")
if !errors.Is(err, ErrLoginRejected) || strings.Contains(err.Error(), "private") {
t.Fatalf("Login() error = %v", err)
}
if manager.Status().Authenticated || manager.Status().CaptchaReady {
t.Fatalf("rejected login state = %+v", manager.Status())
}
if _, err := manager.OpenCaptcha(status.CaptchaTicket); !errors.Is(err, ErrCaptchaTicketInvalid) {
t.Fatalf("OpenCaptcha() after login error = %v", err)
}
manager = testSessionManager(t, server.URL, "test-user", "test-password")
manager.captchaTTL = time.Nanosecond
status, err = manager.FetchCaptcha(context.Background())
if err != nil {
t.Fatalf("short FetchCaptcha() error = %v", err)
}
time.Sleep(time.Millisecond)
if _, err := manager.OpenCaptcha(status.CaptchaTicket); !errors.Is(err, ErrCaptchaTicketInvalid) {
t.Fatalf("expired captcha error = %v", err)
}
manager.authenticated = true
if _, err := manager.Validate(context.Background()); !errors.Is(err, domain.ErrFreightSourceSessionNeeded) ||
manager.Status().Authenticated {
t.Fatalf("expired session error/status = %v / %+v", err, manager.Status())
}
}
func TestSessionManagerSerializesCaptchaRequests(t *testing.T) {
var mutex sync.Mutex
inFlight, maximum := 0, 0
server := httptest.NewServer(http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
if request.URL.Path != CaptchaPath {
writer.WriteHeader(http.StatusNotFound)
return
}
mutex.Lock()
inFlight++
if inFlight > maximum {
maximum = inFlight
}
mutex.Unlock()
time.Sleep(20 * time.Millisecond)
mutex.Lock()
inFlight--
mutex.Unlock()
writer.Header().Set("Content-Type", "image/png")
_, _ = writer.Write([]byte("captcha"))
}))
defer server.Close()
manager := testSessionManager(t, server.URL, "test-user", "test-password")
var group sync.WaitGroup
for range 2 {
group.Add(1)
go func() {
defer group.Done()
if _, err := manager.FetchCaptcha(context.Background()); err != nil {
t.Errorf("FetchCaptcha() error = %v", err)
}
}()
}
group.Wait()
if maximum != 1 {
t.Fatalf("maximum concurrent ERP requests = %d", maximum)
}
}
func testSessionManager(
t *testing.T,
baseURL, username, password string,
) *SessionManager {
t.Helper()
manager, err := NewSessionManager(SessionConfig{
BaseURL: baseURL,
Username: username,
Password: password,
Timeout: time.Second,
AllowInsecureHTTP: true,
})
if err != nil {
t.Fatalf("NewSessionManager() error = %v", err)
}
return manager
}
func assertERPHeaders(t *testing.T, request *http.Request) {
t.Helper()
if request.Header.Get("Accept") != "application/json, text/plain, */*" ||
request.Header.Get("X-Requested-With") != "XMLHttpRequest" ||
request.Header.Get("Origin") == "" || request.Header.Get("Referer") == "" {
t.Fatalf("ERP headers = %#v", request.Header)
}
}