2026-04-25 13:21:39 +08:00

232 lines
9.1 KiB
Go

package auth_test
import (
"context"
"testing"
"time"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
authservice "hyapp/services/user-service/internal/service/auth"
userservice "hyapp/services/user-service/internal/service/user"
"hyapp/services/user-service/internal/storage/memory"
)
type sequenceIDGenerator struct {
values []int64
index int
}
func (g *sequenceIDGenerator) NewInt64() int64 {
value := g.values[g.index]
if g.index < len(g.values)-1 {
g.index++
}
return value
}
type sequenceDisplayUserIDAllocator struct {
values []string
}
func (a sequenceDisplayUserIDAllocator) NextDisplayUserID(_ int64, attempt int) string {
if attempt >= len(a.values) {
return a.values[len(a.values)-1]
}
return a.values[attempt]
}
func newAuthService(repository *memory.Repository, now *time.Time, ids []int64, displayIDs []string) *authservice.Service {
return authservice.New(authservice.Config{
Issuer: "hyapp-test",
AccessTokenTTLSec: 1800,
RefreshTokenTTLSec: 2592000,
SigningAlg: "HS256",
SigningSecret: "test-secret",
},
authservice.WithRepository(repository),
authservice.WithIDGenerator(&sequenceIDGenerator{values: ids}),
authservice.WithDisplayUserIDAllocator(sequenceDisplayUserIDAllocator{values: displayIDs}),
authservice.WithClock(func() time.Time { return *now }),
authservice.WithThirdPartyVerifier(authservice.NewStaticThirdPartyVerifier([]string{"wechat"})),
)
}
func TestThirdPartyUserSetsPasswordThenLogsIn(t *testing.T) {
ctx := context.Background()
repository := memory.NewRepository()
now := time.UnixMilli(1000)
svc := newAuthService(repository, &now, []int64{900001}, []string{"100001"})
thirdToken, isNewUser, err := svc.LoginThirdParty(ctx, "wechat", "openid-1", "ios", authservice.Meta{RequestID: "req-third"})
if err != nil {
t.Fatalf("LoginThirdParty failed: %v", err)
}
if !isNewUser || thirdToken.UserID != 900001 || thirdToken.DisplayUserID != "100001" || thirdToken.RefreshToken == "" || thirdToken.AccessToken == "" {
t.Fatalf("unexpected third-party token: token=%+v isNew=%v", thirdToken, isNewUser)
}
if _, err := svc.LoginPassword(ctx, "100001", "secret-pass", "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.AuthFailed) {
t.Fatalf("expected AUTH_FAILED before password is set, got %v", err)
}
if err := svc.SetPassword(ctx, thirdToken.UserID, "secret-pass", authservice.Meta{RequestID: "req-set-password"}); err != nil {
t.Fatalf("SetPassword failed: %v", err)
}
if err := svc.SetPassword(ctx, thirdToken.UserID, "another-pass", authservice.Meta{}); !xerr.IsCode(err, xerr.PasswordAlreadySet) {
t.Fatalf("expected PASSWORD_ALREADY_SET, got %v", err)
}
loginToken, err := svc.LoginPassword(ctx, "100001", "secret-pass", "ios", authservice.Meta{RequestID: "req-login"})
if err != nil {
t.Fatalf("LoginPassword failed: %v", err)
}
if loginToken.UserID != thirdToken.UserID || loginToken.DisplayUserID != "100001" || loginToken.RefreshToken == thirdToken.RefreshToken {
t.Fatalf("unexpected login token: %+v", loginToken)
}
if _, err := svc.LoginPassword(ctx, "100001", "wrong", "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.AuthFailed) {
t.Fatalf("expected AUTH_FAILED for wrong password, got %v", err)
}
now = now.Add(time.Second)
refreshToken, err := svc.RefreshToken(ctx, loginToken.RefreshToken, "ios", authservice.Meta{RequestID: "req-refresh"})
if err != nil {
t.Fatalf("RefreshToken failed: %v", err)
}
if refreshToken.RefreshToken == loginToken.RefreshToken || refreshToken.DisplayUserID != "100001" {
t.Fatalf("refresh token must rotate")
}
if _, err := svc.RefreshToken(ctx, loginToken.RefreshToken, "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.SessionRevoked) {
t.Fatalf("expected old refresh token revoked, got %v", err)
}
revoked, err := svc.Logout(ctx, refreshToken.SessionID, refreshToken.RefreshToken, authservice.Meta{RequestID: "req-logout"})
if err != nil || !revoked {
t.Fatalf("Logout failed: revoked=%v err=%v", revoked, err)
}
if _, err := svc.RefreshToken(ctx, refreshToken.RefreshToken, "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.SessionRevoked) {
t.Fatalf("expected logout refresh token revoked, got %v", err)
}
}
func TestDisabledUserCannotLogin(t *testing.T) {
ctx := context.Background()
repository := memory.NewRepository()
now := time.UnixMilli(1000)
svc := newAuthService(repository, &now, []int64{900001}, []string{"100001"})
token, _, err := svc.LoginThirdParty(ctx, "wechat", "openid-1", "ios", authservice.Meta{})
if err != nil {
t.Fatalf("LoginThirdParty failed: %v", err)
}
if err := svc.SetPassword(ctx, token.UserID, "secret-pass", authservice.Meta{}); err != nil {
t.Fatalf("SetPassword failed: %v", err)
}
user, err := repository.GetUser(ctx, token.UserID)
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
user.Status = userdomain.StatusDisabled
repository.PutUser(user)
_, err = svc.LoginPassword(ctx, "100001", "secret-pass", "ios", authservice.Meta{})
if !xerr.IsCode(err, xerr.UserDisabled) {
t.Fatalf("expected USER_DISABLED, got %v", err)
}
}
func TestThirdPartyLoginOrRegister(t *testing.T) {
ctx := context.Background()
repository := memory.NewRepository()
now := time.UnixMilli(1000)
svc := newAuthService(repository, &now, []int64{900002}, []string{"100002"})
firstToken, isNewUser, err := svc.LoginThirdParty(ctx, "wechat", "openid-1", "ios", authservice.Meta{RequestID: "req-third-first"})
if err != nil {
t.Fatalf("first LoginThirdParty failed: %v", err)
}
if !isNewUser || firstToken.UserID != 900002 || firstToken.DisplayUserID != "100002" {
t.Fatalf("unexpected first third-party result: token=%+v isNew=%v", firstToken, isNewUser)
}
secondToken, isNewUser, err := svc.LoginThirdParty(ctx, "wechat", "openid-1", "ios", authservice.Meta{RequestID: "req-third-second"})
if err != nil {
t.Fatalf("second LoginThirdParty failed: %v", err)
}
if isNewUser || secondToken.UserID != firstToken.UserID {
t.Fatalf("expected existing third-party user, token=%+v isNew=%v", secondToken, isNewUser)
}
if _, _, err := svc.LoginThirdParty(ctx, "unknown", "openid-1", "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.AuthFailed) {
t.Fatalf("expected AUTH_FAILED for unknown provider, got %v", err)
}
}
func TestThirdPartyRegisterRetriesDisplayUserIDConflict(t *testing.T) {
ctx := context.Background()
repository := memory.NewRepository()
repository.PutUser(userdomain.User{UserID: 1, CurrentDisplayUserID: "100001", Status: userdomain.StatusActive})
now := time.UnixMilli(1000)
svc := newAuthService(repository, &now, []int64{900001, 900002}, []string{"100001", "100002"})
token, _, err := svc.LoginThirdParty(ctx, "wechat", "openid-1", "ios", authservice.Meta{})
if err != nil {
t.Fatalf("LoginThirdParty should retry display_user_id conflict: %v", err)
}
userSvc := userservice.New(repository)
identity, err := userSvc.GetUserIdentity(ctx, token.UserID)
if err != nil {
t.Fatalf("GetUserIdentity failed: %v", err)
}
if identity.DisplayUserID != "100002" {
t.Fatalf("display_user_id retry mismatch: %+v", identity)
}
}
func TestPasswordLoginUsesCurrentDisplayUserIDWithPrettyLease(t *testing.T) {
ctx := context.Background()
repository := memory.NewRepository()
now := time.UnixMilli(1000)
authSvc := newAuthService(repository, &now, []int64{900001}, []string{"100001"})
userSvc := userservice.New(repository, userservice.WithClock(func() time.Time { return now }))
token, _, err := authSvc.LoginThirdParty(ctx, "wechat", "openid-pretty", "ios", authservice.Meta{})
if err != nil {
t.Fatalf("LoginThirdParty failed: %v", err)
}
if err := authSvc.SetPassword(ctx, token.UserID, "secret-pass", authservice.Meta{}); err != nil {
t.Fatalf("SetPassword failed: %v", err)
}
if _, _, err := userSvc.ApplyPrettyDisplayUserID(ctx, token.UserID, "888888", 1000, "receipt-1", "req-pretty"); err != nil {
t.Fatalf("ApplyPrettyDisplayUserID failed: %v", err)
}
if _, err := authSvc.LoginPassword(ctx, "100001", "secret-pass", "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.AuthFailed) {
t.Fatalf("default display_user_id must not login while pretty is active, got %v", err)
}
prettyToken, err := authSvc.LoginPassword(ctx, "888888", "secret-pass", "ios", authservice.Meta{})
if err != nil {
t.Fatalf("pretty display_user_id login failed: %v", err)
}
if prettyToken.DisplayUserID != "888888" || prettyToken.DefaultDisplayUserID != "100001" || prettyToken.DisplayUserIDKind != string(userdomain.DisplayUserIDKindPretty) {
t.Fatalf("unexpected pretty login token: %+v", prettyToken)
}
now = time.UnixMilli(2500)
defaultToken, err := authSvc.LoginPassword(ctx, "100001", "secret-pass", "ios", authservice.Meta{})
if err != nil {
t.Fatalf("default display_user_id login should recover after pretty expires: %v", err)
}
if defaultToken.DisplayUserID != "100001" || defaultToken.DisplayUserIDKind != string(userdomain.DisplayUserIDKindDefault) {
t.Fatalf("unexpected recovered login token: %+v", defaultToken)
}
if _, err := authSvc.LoginPassword(ctx, "888888", "secret-pass", "ios", authservice.Meta{}); !xerr.IsCode(err, xerr.AuthFailed) {
t.Fatalf("expired pretty display_user_id must not login, got %v", err)
}
}