Files
ttstd-infinite-canvas/backend/internal/auth/login_logs_test.go
2026-10-13 05:31:30 +08:00

105 lines
4.0 KiB
Go

package auth
import (
"encoding/json"
"strings"
"testing"
"time"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
)
func TestLoginLogMasksAttemptedContacts(t *testing.T) {
for _, account := range []string{"unknown@example.com", "+8613812345678", "1234567", "\n1234567\t"} {
masked := MaskLoginAccount(account)
if !strings.Contains(masked, "*") || strings.Contains(masked, "unknown") || strings.Contains(masked, "1234567") || strings.ContainsAny(masked, "\n\t") {
t.Fatalf("contact not masked: %q", masked)
}
}
}
func TestLoginLogsExcludeIncompleteProfileReauthentication(t *testing.T) {
s, db, user, cookie := newProfileTestService(t)
state := model.OAuthState{ID: "reauth-state", Provider: "linuxdo", StateHash: HashToken("profile-reauth-state"), ReauthUserID: user.ID, ReauthSessionID: "session-one", ExpiresAt: time.Now().Add(time.Hour)}
if err := db.Create(&state).Error; err != nil {
t.Fatal(err)
}
if _, err := s.CompleteLinuxDOLogin("profile-reauth-state", "", cookie); err == nil {
t.Fatal("missing code accepted")
}
var count int64
if err := db.Model(&model.LoginLog{}).Count(&count).Error; err != nil || count != 0 {
t.Fatalf("profile reauthentication logged as login: %d %v", count, err)
}
}
func TestLoginLogRegistrationSource(t *testing.T) {
s, db := newRegistrationTestService(t)
result, err := s.Register(RegisterRequest{Username: "creator", Password: "test-password", AcceptedTerms: true, LoginContext: LoginContext{IP: "192.0.2.2", UserAgent: "test-client"}})
if err != nil {
t.Fatal(err)
}
var event model.LoginLog
if err := db.First(&event).Error; err != nil {
t.Fatal(err)
}
if event.Source != "registration" || event.Result != "success" || event.UserID == nil || *event.UserID != result.User.ID || event.IP != "192.0.2.2" {
t.Fatalf("registration log: %#v", event)
}
if event.SessionRef == "" || strings.Contains(result.Session, event.SessionRef) {
t.Fatal("session ref exposes cookie identifier")
}
}
func TestLoginLogVerificationMethodsAndInvalidCode(t *testing.T) {
for _, method := range []string{"email", "sms"} {
for _, valid := range []bool{true, false} {
t.Run(method+map[bool]string{true: "/success", false: "/failure"}[valid], func(t *testing.T) {
s, db, u, _ := newProfileTestService(t)
policy := VerificationPolicy{SMSLogin: true, EmailLogin: true}
raw, _ := json.Marshal(policy)
if err := db.Create(&model.SystemSetting{Key: authPolicyKey, ValueJSON: string(raw)}).Error; err != nil {
t.Fatal(err)
}
secret := strings.Repeat("s", 64)
v := model.AuthVerification{ID: "login-verification", TokenHash: HashToken(secret), Purpose: "login", Method: method, UserID: u.ID, CredentialHash: repository.ProfileCredentialHash(u.PasswordHash), PolicyHash: policy.hash(), Ready: true, ExpiresAt: time.Now().Add(time.Hour)}
if method == "email" {
v.Email = u.Email
v.EmailHash, _ = s.verificationHash(v.ID + ":email:" + v.Email + ":123456")
if err := db.Model(u).Update("email_verified_at", time.Now()).Error; err != nil {
t.Fatal(err)
}
} else {
v.Phone = "+8613812345678"
v.PhoneHash, _ = s.verificationHash(v.ID + ":sms:" + v.Phone + ":123456")
if err := db.Model(u).Updates(map[string]any{"phone": v.Phone, "phone_verified_at": time.Now()}).Error; err != nil {
t.Fatal(err)
}
}
if err := db.Create(&v).Error; err != nil {
t.Fatal(err)
}
code := "123456"
if !valid {
code = "000000"
}
_, err := s.LoginVerification(VerificationConfirm{Ticket: v.ID + "." + secret, EmailCode: code, SMSCode: code, LoginContext: LoginContext{IP: "192.0.2.3"}})
if (err == nil) != valid {
t.Fatalf("verification error: %v", err)
}
var event model.LoginLog
if err := db.First(&event).Error; err != nil {
t.Fatal(err)
}
want := "success"
if !valid {
want = "failed"
}
if event.Method != method || event.Result != want || event.UserID == nil || *event.UserID != u.ID {
t.Fatalf("verification log: %#v", event)
}
})
}
}
}