package biz import ( "context" "errors" "testing" "time" "golang.org/x/crypto/bcrypt" ) type authenticationUserRepo struct { UserRepo user *User } func (r *authenticationUserRepo) FindUserByUsername(context.Context, string) (*User, error) { return r.user, nil } func (*authenticationUserRepo) HasAuthorityMenu(context.Context, uint, string) (bool, error) { return true, nil } type authenticationSecurityRepo struct{ SecurityRepo } func (*authenticationSecurityRepo) SecurityConfig(context.Context) (*SecurityConfig, error) { return &SecurityConfig{CaptchaOpen: 2, CaptchaTimeout: 60}, nil } type authenticationCache struct{ activeErr error } func (c *authenticationCache) Get(_ context.Context, key string) (string, bool, error) { if key == "admin" { return "", false, c.activeErr } return "", false, nil } func (*authenticationCache) Set(context.Context, string, string, time.Duration) error { return nil } func (*authenticationCache) Delete(context.Context, string) error { return nil } func (*authenticationCache) Increment(context.Context, string, time.Duration) (int64, error) { return 1, nil } type authenticationSettings struct{ RuntimeSettings } func (*authenticationSettings) UseMultipoint() bool { return true } type authenticationIssuer struct{ TokenIssuer } func (*authenticationIssuer) IssueToken(*User, uint, bool, time.Duration) (*IssuedToken, error) { return &IssuedToken{Value: "token", ExpiresAt: time.Now().Add(time.Hour), TTL: time.Hour}, nil } func (*authenticationIssuer) ReissueToken(claims *AuthClaims, _ uint) (*IssuedToken, error) { return &IssuedToken{Value: "token", ExpiresAt: claims.ExpiresAt, TTL: time.Until(claims.ExpiresAt)}, nil } type authenticationAudit struct { AuditRecordRepo logins []*LoginLog } type switchAuthorityUserRepo struct { UserRepo setUserID, setAuthorityID uint findCalls int } func (r *switchAuthorityUserRepo) SetUserAuthority(_ context.Context, userID, authorityID uint) error { r.setUserID, r.setAuthorityID = userID, authorityID return nil } func (r *switchAuthorityUserRepo) FindUserByID(context.Context, uint) (*User, error) { r.findCalls++ return &User{NickName: "changed-in-database"}, nil } type switchAuthorityIssuer struct { TokenIssuer claims *AuthClaims authorityID uint } func (i *switchAuthorityIssuer) ReissueToken(claims *AuthClaims, authorityID uint) (*IssuedToken, error) { i.claims, i.authorityID = claims, authorityID return &IssuedToken{Value: "switched", ExpiresAt: claims.ExpiresAt, TTL: time.Until(claims.ExpiresAt)}, nil } func (a *authenticationAudit) RecordLogin(_ context.Context, value *LoginLog) error { a.logins = append(a.logins, value) return nil } func TestLoginRecordsSuccessBeforeMultipointCacheFailure(t *testing.T) { hash, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost) if err != nil { t.Fatal(err) } users := NewUserUsecase(&authenticationUserRepo{user: &User{ID: 1, Username: "admin", Password: string(hash), AuthorityID: 888, Enable: 1}}) cacheErr := errors.New("cache unavailable") security := NewSecurityUsecase(&authenticationSecurityRepo{}, &authenticationCache{activeErr: cacheErr}, &authenticationSettings{}, nil) audit := &authenticationAudit{} uc := NewAuthenticationUsecase(users, security, &authenticationIssuer{}, audit) _, err = uc.Login(context.Background(), &LoginAttempt{Username: "admin", Password: "secret", IP: "127.0.0.1", Agent: "test"}) if !errors.Is(err, ErrLoginState) { t.Fatalf("expected login-state failure, got %v", err) } if len(audit.logins) != 1 || !audit.logins[0].Status || audit.logins[0].ErrorMessage != "登录成功" || audit.logins[0].UserID != 1 { t.Fatalf("expected successful login audit before cache failure, got %+v", audit.logins) } } func TestSwitchAuthorityReissuesCurrentClaimsWithoutReloadingUser(t *testing.T) { repo := &switchAuthorityUserRepo{} issuer := &switchAuthorityIssuer{} uc := NewAuthenticationUsecase(NewUserUsecase(repo), nil, issuer, nil) expiresAt := time.Now().Add(time.Hour).Truncate(time.Second) claims := &AuthClaims{ID: 7, UUID: "uuid", Username: "admin", NickName: "token-nickname", AuthorityID: 888, MustChangePwd: true, ExpiresAt: expiresAt} result, err := uc.SwitchAuthority(context.Background(), claims, 999) if err != nil { t.Fatal(err) } if repo.setUserID != 7 || repo.setAuthorityID != 999 { t.Fatalf("stored authority switch = user:%d authority:%d", repo.setUserID, repo.setAuthorityID) } if repo.findCalls != 0 { t.Fatalf("switch reloaded user %d times", repo.findCalls) } if issuer.claims != claims || issuer.authorityID != 999 { t.Fatalf("reissue input = claims:%p authority:%d", issuer.claims, issuer.authorityID) } if result.User.NickName != "token-nickname" || result.User.AuthorityID != 999 || !result.NeedChangePassword || !result.ExpiresAt.Equal(expiresAt) { t.Fatalf("switch result = %+v", result) } }