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) } }