76 lines
2.8 KiB
Go
76 lines
2.8 KiB
Go
package externaladmin
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"hyapp-admin-server/internal/security"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestLoginFailureLimitPersistsTimedLockAndUnifiedError(t *testing.T) {
|
|
adminSQL, adminMock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("create admin sql mock: %v", err)
|
|
}
|
|
defer adminSQL.Close()
|
|
adminDB, err := gorm.Open(mysql.New(mysql.Config{Conn: adminSQL, SkipInitializeWithVersion: true}), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("create admin gorm db: %v", err)
|
|
}
|
|
userDB, userMock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("create user sql mock: %v", err)
|
|
}
|
|
defer userDB.Close()
|
|
|
|
passwordHash, err := security.HashPassword("correct-password")
|
|
if err != nil {
|
|
t.Fatalf("hash fixture password: %v", err)
|
|
}
|
|
userMock.ExpectQuery("SELECT app_code, app_name, logo_url FROM apps").
|
|
WithArgs("fami").
|
|
WillReturnRows(sqlmock.NewRows([]string{"app_code", "app_name", "logo_url"}).AddRow("fami", "Fami", "logo"))
|
|
adminMock.ExpectBegin()
|
|
adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE app_code = \\? AND username = \\?").
|
|
WithArgs("fami", "operator", 1).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "app_code", "linked_app_user_id", "username", "password_hash", "permissions_json", "status",
|
|
"password_change_required", "failed_login_count", "locked_until_ms", "created_by_admin_id", "created_at_ms", "updated_at_ms",
|
|
}).AddRow(7, "fami", 9001, "operator", passwordHash, `[]`, "active", false, 0, 0, 1, 1, 1))
|
|
adminMock.ExpectExec("UPDATE `external_admin_accounts` SET").
|
|
WithArgs(uint(0), futureMillis{}, sqlmock.AnyArg(), uint64(7)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
adminMock.ExpectExec("INSERT INTO `external_admin_login_logs`").
|
|
WithArgs(sqlmock.AnyArg(), "fami", "operator", "127.0.0.1", "browser", "failed", "failure_limit_locked", sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
adminMock.ExpectCommit()
|
|
|
|
service := NewService(adminDB, userDB, Config{MaxLoginFailures: 1, LockDuration: 10 * time.Minute})
|
|
service.limiter = allowFixedWindowLimiter{}
|
|
_, err = service.Login(t.Context(), LoginInput{
|
|
AppCode: "FAMI", Username: "Operator", Password: "wrong-password", IP: "127.0.0.1", UserAgent: "browser",
|
|
})
|
|
if !errors.Is(err, ErrInvalidCredentials) {
|
|
t.Fatalf("login error = %v, want unified invalid credentials", err)
|
|
}
|
|
if err := adminMock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("admin sql expectations: %v", err)
|
|
}
|
|
if err := userMock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("user sql expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
type futureMillis struct{}
|
|
|
|
func (futureMillis) Match(value driver.Value) bool {
|
|
millis, ok := value.(int64)
|
|
return ok && millis > time.Now().UTC().UnixMilli()
|
|
}
|