feat(t226): add Go ERP session login
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
package shunyunbao
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSessionTimeout = 30 * time.Second
|
||||
defaultCaptchaTTL = 5 * time.Minute
|
||||
maxCaptchaBytes = 2 << 20
|
||||
maxERPResponseBytes = 4 << 20
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCaptchaTicketInvalid = errors.New("ERP captcha ticket is invalid")
|
||||
ErrLoginRejected = errors.New("ERP login was rejected")
|
||||
)
|
||||
|
||||
type SessionConfig struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
Timeout time.Duration
|
||||
CaptchaTTL time.Duration
|
||||
AllowInsecureHTTP bool // Used only by isolated httptest contracts.
|
||||
}
|
||||
|
||||
type SessionStatus struct {
|
||||
Configured bool
|
||||
Authenticated bool
|
||||
CaptchaReady bool
|
||||
CaptchaTicket string
|
||||
}
|
||||
|
||||
type CaptchaImage struct {
|
||||
Content []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type SessionManager struct {
|
||||
mu sync.Mutex
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
timeout time.Duration
|
||||
captchaTTL time.Duration
|
||||
headers http.Header
|
||||
http *http.Client
|
||||
authenticated bool
|
||||
captchaTicket string
|
||||
captchaContent []byte
|
||||
captchaType string
|
||||
captchaExpires time.Time
|
||||
}
|
||||
|
||||
func NewSessionManager(config SessionConfig) (*SessionManager, error) {
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(config.BaseURL), "/")
|
||||
headers, err := RequestHeaders(baseURL)
|
||||
if err != nil {
|
||||
return nil, errors.New("shunyunbao session configuration is invalid")
|
||||
}
|
||||
parsed, _ := url.Parse(baseURL)
|
||||
if parsed.Scheme != "https" && !config.AllowInsecureHTTP {
|
||||
return nil, errors.New("shunyunbao session requires HTTPS")
|
||||
}
|
||||
username := strings.TrimSpace(config.Username)
|
||||
if (username == "") != (config.Password == "") ||
|
||||
!utf8.ValidString(username) || hasControl(username) ||
|
||||
strings.ContainsRune(config.Password, '\x00') {
|
||||
return nil, errors.New("shunyunbao credentials are invalid")
|
||||
}
|
||||
timeout := config.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultSessionTimeout
|
||||
}
|
||||
captchaTTL := config.CaptchaTTL
|
||||
if captchaTTL <= 0 {
|
||||
captchaTTL = defaultCaptchaTTL
|
||||
}
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("create shunyunbao cookie jar")
|
||||
}
|
||||
return &SessionManager{
|
||||
baseURL: baseURL,
|
||||
username: username,
|
||||
password: config.Password,
|
||||
timeout: timeout,
|
||||
captchaTTL: captchaTTL,
|
||||
headers: headers,
|
||||
http: &http.Client{
|
||||
Jar: jar,
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Status() SessionStatus {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
manager.expireCaptchaLocked(time.Now())
|
||||
return manager.statusLocked()
|
||||
}
|
||||
|
||||
func (manager *SessionManager) FetchCaptcha(
|
||||
ctx context.Context,
|
||||
) (SessionStatus, error) {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
if !manager.configuredLocked() {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceNotConfigured
|
||||
}
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodGet,
|
||||
manager.baseURL+CaptchaPath+"?_="+strconv.FormatInt(time.Now().UnixMilli(), 10),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
manager.applyHeaders(request)
|
||||
response, err := manager.http.Do(request)
|
||||
if err != nil {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return manager.statusLocked(), manager.responseErrorLocked(response.StatusCode)
|
||||
}
|
||||
contentType := strings.TrimSpace(
|
||||
strings.Split(response.Header.Get("Content-Type"), ";")[0],
|
||||
)
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceProtocol
|
||||
}
|
||||
content, err := readBounded(response.Body, maxCaptchaBytes)
|
||||
if err != nil || len(content) == 0 {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
ticket, err := newCaptchaTicket()
|
||||
if err != nil {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
manager.captchaTicket = ticket
|
||||
manager.captchaContent = content
|
||||
manager.captchaType = contentType
|
||||
manager.captchaExpires = time.Now().Add(manager.captchaTTL)
|
||||
return manager.statusLocked(), nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) OpenCaptcha(
|
||||
ticket string,
|
||||
) (CaptchaImage, error) {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
manager.expireCaptchaLocked(time.Now())
|
||||
if !manager.captchaMatchesLocked(ticket) {
|
||||
return CaptchaImage{}, ErrCaptchaTicketInvalid
|
||||
}
|
||||
return CaptchaImage{
|
||||
Content: append([]byte(nil), manager.captchaContent...),
|
||||
ContentType: manager.captchaType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Login(
|
||||
ctx context.Context,
|
||||
ticket, captchaCode string,
|
||||
) (SessionStatus, error) {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
if !manager.configuredLocked() {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceNotConfigured
|
||||
}
|
||||
manager.expireCaptchaLocked(time.Now())
|
||||
if !manager.captchaMatchesLocked(ticket) || !validCaptchaCode(captchaCode) {
|
||||
return manager.statusLocked(), ErrCaptchaTicketInvalid
|
||||
}
|
||||
defer manager.clearCaptchaLocked()
|
||||
manager.authenticated = false
|
||||
payload, err := json.Marshal(map[string]string{
|
||||
"username": manager.username,
|
||||
"password": manager.password,
|
||||
"code": strings.TrimSpace(captchaCode),
|
||||
})
|
||||
if err != nil {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceProtocol
|
||||
}
|
||||
data, err := manager.requestJSONLocked(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
LoginPath,
|
||||
payload,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
return manager.statusLocked(), err
|
||||
}
|
||||
if !hasUser(data) {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceProtocol
|
||||
}
|
||||
if _, err := manager.validateLocked(ctx); err != nil {
|
||||
return manager.statusLocked(), err
|
||||
}
|
||||
manager.authenticated = true
|
||||
manager.clearCaptchaLocked()
|
||||
return manager.statusLocked(), nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) Validate(
|
||||
ctx context.Context,
|
||||
) (SessionStatus, error) {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
if !manager.configuredLocked() {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceNotConfigured
|
||||
}
|
||||
if !manager.authenticated {
|
||||
return manager.statusLocked(), domain.ErrFreightSourceSessionNeeded
|
||||
}
|
||||
if _, err := manager.validateLocked(ctx); err != nil {
|
||||
return manager.statusLocked(), err
|
||||
}
|
||||
return manager.statusLocked(), nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) validateLocked(ctx context.Context) (any, error) {
|
||||
data, err := manager.requestJSONLocked(
|
||||
ctx,
|
||||
http.MethodGet,
|
||||
UserPath,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasUser(data) {
|
||||
return nil, domain.ErrFreightSourceProtocol
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) requestJSONLocked(
|
||||
ctx context.Context,
|
||||
method, path string,
|
||||
body []byte,
|
||||
loginRequest bool,
|
||||
) (any, error) {
|
||||
var content io.Reader
|
||||
if body != nil {
|
||||
content = bytes.NewReader(body)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
method,
|
||||
manager.baseURL+path,
|
||||
content,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
manager.applyHeaders(request)
|
||||
if body != nil {
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
response, err := manager.http.Do(request)
|
||||
if err != nil {
|
||||
return nil, domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
||||
if loginRequest {
|
||||
return nil, ErrLoginRejected
|
||||
}
|
||||
return nil, manager.responseErrorLocked(response.StatusCode)
|
||||
}
|
||||
contentBytes, err := readBounded(response.Body, maxERPResponseBytes)
|
||||
if err != nil {
|
||||
return nil, domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
var envelope struct {
|
||||
Status bool `json:"status"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(contentBytes))
|
||||
if err := decoder.Decode(&envelope); err != nil || len(envelope.Data) == 0 {
|
||||
return nil, domain.ErrFreightSourceProtocol
|
||||
}
|
||||
if !envelope.Status {
|
||||
if loginRequest {
|
||||
return nil, ErrLoginRejected
|
||||
}
|
||||
manager.authenticated = false
|
||||
return nil, domain.ErrFreightSourceSessionNeeded
|
||||
}
|
||||
var data any
|
||||
dataDecoder := json.NewDecoder(bytes.NewReader(envelope.Data))
|
||||
dataDecoder.UseNumber()
|
||||
if err := dataDecoder.Decode(&data); err != nil {
|
||||
return nil, domain.ErrFreightSourceProtocol
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (manager *SessionManager) applyHeaders(request *http.Request) {
|
||||
for name, values := range manager.headers {
|
||||
request.Header[name] = append([]string(nil), values...)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *SessionManager) responseErrorLocked(status int) error {
|
||||
if status == http.StatusUnauthorized || status == http.StatusForbidden {
|
||||
manager.authenticated = false
|
||||
return domain.ErrFreightSourceSessionNeeded
|
||||
}
|
||||
return domain.ErrFreightSourceUnavailable
|
||||
}
|
||||
|
||||
func (manager *SessionManager) configuredLocked() bool {
|
||||
return manager.username != "" && manager.password != ""
|
||||
}
|
||||
|
||||
func (manager *SessionManager) statusLocked() SessionStatus {
|
||||
return SessionStatus{
|
||||
Configured: manager.configuredLocked(),
|
||||
Authenticated: manager.authenticated,
|
||||
CaptchaReady: manager.captchaTicket != "",
|
||||
CaptchaTicket: manager.captchaTicket,
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *SessionManager) captchaMatchesLocked(ticket string) bool {
|
||||
return ticket != "" && ticket == manager.captchaTicket
|
||||
}
|
||||
|
||||
func (manager *SessionManager) expireCaptchaLocked(now time.Time) {
|
||||
if manager.captchaTicket != "" && !now.Before(manager.captchaExpires) {
|
||||
manager.clearCaptchaLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *SessionManager) clearCaptchaLocked() {
|
||||
manager.captchaTicket = ""
|
||||
manager.captchaContent = nil
|
||||
manager.captchaType = ""
|
||||
manager.captchaExpires = time.Time{}
|
||||
}
|
||||
|
||||
func hasUser(value any) bool {
|
||||
data, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if user, exists := data["user"]; exists {
|
||||
_, ok := user.(map[string]any)
|
||||
return ok
|
||||
}
|
||||
return data["id"] != nil || data["username"] != nil
|
||||
}
|
||||
|
||||
func validCaptchaCode(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
return value != "" && len([]byte(value)) <= 64 && utf8.ValidString(value) &&
|
||||
!hasControl(value)
|
||||
}
|
||||
|
||||
func newCaptchaTicket() (string, error) {
|
||||
value := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(value), nil
|
||||
}
|
||||
|
||||
func readBounded(reader io.Reader, maximum int64) ([]byte, error) {
|
||||
content, err := io.ReadAll(io.LimitReader(reader, maximum+1))
|
||||
if err != nil || int64(len(content)) > maximum {
|
||||
return nil, errors.New("response exceeds limit")
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user