package service import ( "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wangjia/jiu/backend/config" "github.com/wangjia/jiu/backend/internal/model" "github.com/wangjia/jiu/backend/testutil" ) func TestAuthService_Login_Success(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "HOTEL001") testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin") svc := NewAuthService(db) pair, user, err := svc.Login("HOTEL001", "admin", "password123", DeviceInfo{Platform: "windows"}) require.NoError(t, err) require.NotNil(t, pair) require.NotNil(t, user) assert.NotEmpty(t, pair.AccessToken) assert.NotEmpty(t, pair.RefreshToken) assert.Equal(t, "admin", user.Username) } func TestAuthService_Login_AutoTrialOnFirstUse(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "TRIAL001") testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin") // 前置:门店「未激活」——无任何授权行 var before int64 db.Model(&model.License{}).Where("shop_id = ?", shop.ID).Count(&before) require.EqualValues(t, 0, before) svc := NewAuthService(db) // 首次登录 → 自动签发 30 天 trial _, _, err := svc.Login("TRIAL001", "admin", "password123", DeviceInfo{Platform: "windows"}) require.NoError(t, err) var lic model.License require.NoError(t, db.Where("shop_id = ? AND is_active = 1", shop.ID).First(&lic).Error) assert.Equal(t, "trial", lic.Type) assert.True(t, lic.IsActive) require.NotNil(t, lic.ExpiresAt) days := time.Until(*lic.ExpiresAt).Hours() / 24 assert.InDelta(t, 30, days, 1, "试用期应约为 30 天") // 再次登录不重复发放 _, _, err = svc.Login("TRIAL001", "admin", "password123", DeviceInfo{Platform: "windows"}) require.NoError(t, err) var count int64 db.Model(&model.License{}).Where("shop_id = ?", shop.ID).Count(&count) assert.EqualValues(t, 1, count, "已有有效授权时不应再发放 trial") } func TestAuthService_Login_WrongPassword(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "HOTEL002") testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin") svc := NewAuthService(db) pair, user, err := svc.Login("HOTEL002", "admin", "wrongpassword", DeviceInfo{Platform: "windows"}) assert.Error(t, err) assert.Equal(t, ErrInvalidCredentials, err) assert.Nil(t, pair) assert.Nil(t, user) } func TestAuthService_Login_WrongHotel(t *testing.T) { db := testutil.SetupTestDB() testutil.CreateTestShop(db, "HOTEL003") svc := NewAuthService(db) pair, user, err := svc.Login("NONEXISTENT", "admin", "password123", DeviceInfo{Platform: "windows"}) assert.Error(t, err) assert.Equal(t, ErrInvalidCredentials, err) assert.Nil(t, pair) assert.Nil(t, user) } func TestAuthService_Login_DisabledUser(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "HOTEL004") user := testutil.CreateTestUser(db, shop.ID, "disabled", "password123", "operator") // 禁用用户 db.Model(user).Update("is_active", false) svc := NewAuthService(db) pair, u, err := svc.Login("HOTEL004", "disabled", "password123", DeviceInfo{Platform: "windows"}) assert.Error(t, err) assert.Equal(t, ErrUserInactive, err) assert.Nil(t, pair) assert.Nil(t, u) } func TestAuthService_Login_WrongUsername(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "HOTEL005") testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin") svc := NewAuthService(db) pair, user, err := svc.Login("HOTEL005", "nonexistent", "password123", DeviceInfo{Platform: "windows"}) assert.Error(t, err) assert.Equal(t, ErrInvalidCredentials, err) assert.Nil(t, pair) assert.Nil(t, user) } func TestAuthService_RefreshTokens(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "HOTEL006") testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin") svc := NewAuthService(db) pair, _, err := svc.Login("HOTEL006", "admin", "password123", DeviceInfo{Platform: "windows"}) require.NoError(t, err) require.NotNil(t, pair) // 用 refresh token 换新 token newPair, err := svc.RefreshTokens(pair.RefreshToken) require.NoError(t, err) require.NotNil(t, newPair) assert.NotEmpty(t, newPair.AccessToken) assert.NotEmpty(t, newPair.RefreshToken) } func TestAuthService_RefreshTokens_Invalid(t *testing.T) { db := testutil.SetupTestDB() testutil.InitConfig() svc := NewAuthService(db) newPair, err := svc.RefreshTokens("invalid.token.here") assert.Error(t, err) assert.Nil(t, newPair) } // TestLogin_AccountLockoutAfterMaxFailures 同一账号连续失败达阈值后锁定(回归)。 func TestLogin_AccountLockoutAfterMaxFailures(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "LOCK_ACC") testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin") config.C.Session.MaxFailures = 3 svc := NewAuthService(db) defer loginLim.reset("LOCK_ACC|admin") for i := 0; i < 3; i++ { _, _, err := svc.Login("LOCK_ACC", "admin", "wrong", DeviceInfo{Platform: "windows", IP: "10.0.0.1"}) require.ErrorIs(t, err, ErrInvalidCredentials) } // 第 4 次即便密码正确也被锁定拒绝。 _, _, err := svc.Login("LOCK_ACC", "admin", "password123", DeviceInfo{Platform: "windows", IP: "10.0.0.1"}) assert.ErrorIs(t, err, ErrTooManyAttempts) } // TestLogin_IPLockoutAcrossAccounts 单 IP 跨多个(不存在的)账号累计失败达 IP 阈值后锁该 IP。 // 每次用不同用户名,账号维度永不触发锁定,只有 IP 维度会锁——验证 per-IP 防撞库 + 防内存灌爆。 func TestLogin_IPLockoutAcrossAccounts(t *testing.T) { db := testutil.SetupTestDB() testutil.CreateTestShop(db, "LOCK_IP") config.C.Session.MaxFailures = 5 config.C.Session.IPMaxFailures = 4 const attackIP = "203.0.113.9" svc := NewAuthService(db) defer loginLim.reset("ip|" + attackIP) // 4 次不同用户名(invalid_user),账号 key 各不相同永不锁;IP key 累计到 4 → 锁 IP。 for i := 0; i < 4; i++ { uname := "ghost" + strconv.Itoa(i) _, _, err := svc.Login("LOCK_IP", uname, "whatever", DeviceInfo{Platform: "windows", IP: attackIP}) require.ErrorIs(t, err, ErrInvalidCredentials) } // 同 IP 再来一发(仍是新用户名,账号维度无锁)→ 被 IP 锁拦下。 _, _, err := svc.Login("LOCK_IP", "ghostX", "whatever", DeviceInfo{Platform: "windows", IP: attackIP}) assert.ErrorIs(t, err, ErrTooManyAttempts) // 另一 IP 不受影响。 _, _, err = svc.Login("LOCK_IP", "ghostY", "whatever", DeviceInfo{Platform: "windows", IP: "198.51.100.7"}) assert.ErrorIs(t, err, ErrInvalidCredentials) } func TestHashPassword(t *testing.T) { hash, err := HashPassword("mypassword") require.NoError(t, err) assert.NotEmpty(t, hash) assert.NotEqual(t, "mypassword", hash) // 第二次哈希应不同(bcrypt 加 salt) hash2, err := HashPassword("mypassword") require.NoError(t, err) assert.NotEqual(t, hash, hash2) } func TestInitConfig(t *testing.T) { testutil.InitConfig() } func init() { testutil.InitConfig() }