Files
web_huangguo2_h5/app/service/authser/guest_enter_test.go
T
谢宇宁andClaude Opus 5.5 0fbdba5ccf Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:47:47 +07:00

346 lines
11 KiB
Go

package authser
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"videoweb/app/appg"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
var authCookieNamesWritten = []string{
"token",
"userId",
"accountNo",
"nickName",
"avatar",
"recharge",
"vipType",
"currentVipExpire",
"userType",
"isRegister",
}
func TestSyncUserOnEnterCreatesGuestWithoutRefreshingIt(t *testing.T) {
var guestCalls atomic.Int32
var getInfoCalls atomic.Int32
useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case guestPath:
guestCalls.Add(1)
if r.Method != http.MethodPost {
t.Errorf("guest method = %s, want POST", r.Method)
}
var request GuestLoginReq
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Errorf("decode guest request: %v", err)
}
if request.DevID == "" || request.DeviceID != request.DevID {
t.Errorf("guest device ids = %#v, want matching non-empty ids", request)
}
writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("guest-token", "new-guest"))
case getInfoPath:
getInfoCalls.Add(1)
writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("guest-token", "unexpected-refresh"))
default:
t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}
}))
c, recorder := newAuthTestContext()
user := SyncUserOnEnter(c)
if user == nil || user.Token != "guest-token" {
t.Fatalf("SyncUserOnEnter user = %#v, want guest-token", user)
}
if got := guestCalls.Load(); got != 1 {
t.Errorf("guest calls = %d, want 1", got)
}
if got := getInfoCalls.Load(); got != 0 {
t.Errorf("getInfo calls = %d, want 0", got)
}
assertRequestToken(t, c, "guest-token")
assertSingleAuthCookieSet(t, recorder)
}
func TestSyncUserOnEnterRefreshesExistingTokenOnlyOnce(t *testing.T) {
var guestCalls atomic.Int32
var getInfoCalls atomic.Int32
authorization := make(chan string, 1)
useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case guestPath:
guestCalls.Add(1)
writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("unexpected-guest-token", "unexpected-guest"))
case getInfoPath:
getInfoCalls.Add(1)
select {
case authorization <- r.Header.Get("Authorization"):
default:
}
// getInfo may omit token; the existing request token must be preserved.
writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("", "fresh-user"))
default:
t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}
}))
c, recorder := newAuthTestContext(
&http.Cookie{Name: "token", Value: "existing-token"},
&http.Cookie{Name: "sid", Value: "existing-sid"},
)
first := SyncUserOnEnter(c)
second := SyncUserOnEnter(c)
if first == nil || first.Token != "existing-token" || first.NickName != "fresh-user" {
t.Fatalf("first SyncUserOnEnter user = %#v, want refreshed user with existing token", first)
}
if second == nil || second.Token != "existing-token" || second.NickName != "fresh-user" {
t.Fatalf("second SyncUserOnEnter user = %#v, want memoized refreshed user", second)
}
if got := guestCalls.Load(); got != 0 {
t.Errorf("guest calls = %d, want 0", got)
}
if got := getInfoCalls.Load(); got != 1 {
t.Errorf("getInfo calls = %d, want 1", got)
}
select {
case got := <-authorization:
if got != "existing-token" {
t.Errorf("Authorization = %q, want existing-token", got)
}
default:
t.Error("getInfo did not capture Authorization header")
}
assertRequestToken(t, c, "existing-token")
if got := cookieValue(c, "nickName"); got != "fresh-user" {
t.Errorf("request nickName cookie = %q, want fresh-user", got)
}
assertSingleAuthCookieSet(t, recorder)
}
func TestSyncUserOnEnterReusesGuestCreatedBeforeRender(t *testing.T) {
var guestCalls atomic.Int32
var getInfoCalls atomic.Int32
useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case guestPath:
guestCalls.Add(1)
writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("pre-render-token", "pre-render-guest"))
case getInfoPath:
getInfoCalls.Add(1)
writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("pre-render-token", "unexpected-refresh"))
default:
t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}
}))
c, recorder := newAuthTestContext()
guest := EnsureGuestOnEnter(c)
user := SyncUserOnEnter(c)
if guest == nil || guest.Token != "pre-render-token" {
t.Fatalf("EnsureGuestOnEnter user = %#v, want pre-render guest", guest)
}
if user == nil || user.Token != "pre-render-token" || user.NickName != "pre-render-guest" {
t.Fatalf("SyncUserOnEnter user = %#v, want memoized pre-render guest", user)
}
if got := guestCalls.Load(); got != 1 {
t.Errorf("guest calls = %d, want 1", got)
}
if got := getInfoCalls.Load(); got != 0 {
t.Errorf("getInfo calls = %d, want 0", got)
}
assertRequestToken(t, c, "pre-render-token")
assertSingleAuthCookieSet(t, recorder)
}
func TestRefreshUserOnEnterReturnsRebuiltGuestWithoutRefreshingItAgain(t *testing.T) {
var guestCalls atomic.Int32
var getInfoCalls atomic.Int32
getInfoAuthorization := make(chan string, 1)
guestAuthorization := make(chan string, 1)
useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case getInfoPath:
getInfoCalls.Add(1)
select {
case getInfoAuthorization <- r.Header.Get("Authorization"):
default:
}
writeAuthTestEnvelope(w, http.StatusOK, 6001, nil)
case guestPath:
guestCalls.Add(1)
select {
case guestAuthorization <- r.Header.Get("Authorization"):
default:
}
writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("rebuilt-guest-token", "rebuilt-guest"))
default:
t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path)
http.NotFound(w, r)
}
}))
c, recorder := newAuthTestContext(
&http.Cookie{Name: "token", Value: "expired-guest-token"},
&http.Cookie{Name: "isRegister", Value: "0"},
&http.Cookie{Name: "userType", Value: "0"},
)
user := RefreshUserOnEnter(c)
// The rebuilt guest must also be reusable by the page-level sync call.
synced := SyncUserOnEnter(c)
if user == nil || user.Token != "rebuilt-guest-token" || user.NickName != "rebuilt-guest" {
t.Fatalf("RefreshUserOnEnter user = %#v, want rebuilt guest", user)
}
if synced == nil || synced.Token != "rebuilt-guest-token" {
t.Fatalf("SyncUserOnEnter user = %#v, want memoized rebuilt guest", synced)
}
if got := getInfoCalls.Load(); got != 1 {
t.Errorf("getInfo calls = %d, want 1 (no refresh of rebuilt guest)", got)
}
if got := guestCalls.Load(); got != 1 {
t.Errorf("guest calls = %d, want 1", got)
}
select {
case got := <-getInfoAuthorization:
if got != "expired-guest-token" {
t.Errorf("getInfo Authorization = %q, want expired-guest-token", got)
}
default:
t.Error("getInfo did not capture Authorization header")
}
select {
case got := <-guestAuthorization:
if got != "" {
t.Errorf("guest Authorization = %q, want empty after expired token is removed", got)
}
default:
t.Error("guest login did not capture Authorization header")
}
assertRequestToken(t, c, "rebuilt-guest-token")
assertSingleAuthCookieSet(t, recorder)
}
func TestSetAuthCookiesUpsertsSameNameWithinResponse(t *testing.T) {
c, recorder := newAuthTestContext()
SetAuthCookies(c, testLoginUser("first-token", "first-user"))
SetAuthCookies(c, testLoginUser("latest-token", "latest-user"))
assertSingleAuthCookieSet(t, recorder)
assertRequestToken(t, c, "latest-token")
if got := cookieValue(c, "nickName"); got != "latest-user" {
t.Errorf("request nickName cookie = %q, want latest-user", got)
}
for _, cookie := range recorder.Result().Cookies() {
if cookie.Name == "token" && cookie.Value != "latest-token" {
t.Errorf("response token cookie = %q, want latest-token", cookie.Value)
}
}
}
func useAuthTestUpstream(t *testing.T, handler http.Handler) {
t.Helper()
server := httptest.NewServer(handler)
previousServerAddr := appg.Conf.ServerAddr
appg.Conf.ServerAddr = server.URL
t.Cleanup(func() {
appg.Conf.ServerAddr = previousServerAddr
server.Close()
})
}
func newAuthTestContext(cookies ...*http.Cookie) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodGet, "https://site.test/page", nil)
for _, cookie := range cookies {
c.Request.AddCookie(cookie)
}
return c, recorder
}
func writeAuthTestEnvelope(w http.ResponseWriter, status, code int, data *LoginResp) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *LoginResp `json:"data"`
}{
Code: code,
Msg: http.StatusText(status),
Data: data,
})
}
func testLoginUser(token, nickName string) *LoginResp {
return &LoginResp{
Token: token,
UserID: 101,
AccountNo: "account-101",
Email: "user101@example.test",
NickName: nickName,
// This field is intentionally large: it must stay out of Set-Cookie headers.
Introduction: strings.Repeat("简介", 150),
Avatar: "https://cdn.example.test/avatar.png",
InviteCode: "INVITE101",
VipType: 1,
CurrentVipExpire: "2099-01-01T00:00:00Z",
ShowCardID: 7,
ShowCardName: "monthly",
Type: 0,
Recharge: 88,
}
}
func assertRequestToken(t *testing.T, c *gin.Context, want string) {
t.Helper()
if got := useragent.TokenFromRequest(c.Request); got != want {
t.Errorf("request token = %q, want %q", got, want)
}
}
func assertSingleAuthCookieSet(t *testing.T, recorder *httptest.ResponseRecorder) {
t.Helper()
counts := make(map[string]int)
for _, cookie := range recorder.Result().Cookies() {
counts[cookie.Name]++
}
for name, count := range counts {
if count > 1 {
t.Errorf("Set-Cookie %q written %d times, want at most once", name, count)
}
}
for _, name := range authCookieNamesWritten {
if got := counts[name]; got != 1 {
t.Errorf("auth Set-Cookie %q written %d times, want exactly once", name, got)
}
}
for _, name := range []string{"introduction", "email", "inviteCode", "showCardId", "showCardName", "sid"} {
if got := counts[name]; got != 0 {
t.Errorf("non-session Set-Cookie %q written %d times, want none", name, got)
}
}
var headerBytes int
for _, raw := range recorder.Header().Values("Set-Cookie") {
headerBytes += len("Set-Cookie: ") + len(raw) + len("\r\n")
}
if headerBytes >= 4096 {
t.Errorf("Set-Cookie response headers = %d bytes, want below 4096", headerBytes)
}
}