package classes import ( "context" "database/sql" "testing" "time" "git.golem15.com/golem15/fonoteka/plugins/golem15/user/models" "git.golem15.com/golem15/fonoteka/plugins/golem15/user/updates" "git.golem15.com/golem15/summercms/lagoon" "github.com/go-gormigrate/gormigrate/v2" _ "github.com/jackc/pgx/v5/stdlib" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "gorm.io/gorm" ) func TestCheckAndRecordLogin(t *testing.T) { db := throttleDB(t) user := &models.User{Email: "ada@example.test", Password: "x"} if err := db.Create(user).Error; err != nil { t.Fatal(err) } ctx := t.Context() for i := 1; i <= 5; i++ { if err := CheckAndRecordLogin(ctx, db, user, "10.0.0.1", false); err != nil { t.Fatalf("attempt %d: %v", i, err) } } var row models.Throttle if err := db.Where("user_id = ?", user.ID).Take(&row).Error; err != nil { t.Fatal(err) } if row.Attempts != 5 || !row.IsSuspended { t.Fatalf("after 5: %+v", row) } if err := CheckAndRecordLogin(ctx, db, user, "10.0.0.1", false); err == nil { t.Fatal("6th attempt must fail") } past := time.Now().Add(-16 * time.Minute) if err := db.Model(&row).Updates(map[string]any{"is_suspended": true, "suspended_at": past, "attempts": 5}).Error; err != nil { t.Fatal(err) } if err := CheckAndRecordLogin(ctx, db, user, "10.0.0.1", false); err != nil { t.Fatalf("after window: %v", err) } if err := db.Model(&models.Throttle{}).Where("user_id = ?", user.ID).Update("is_banned", true).Error; err != nil { t.Fatal(err) } if err := CheckAndRecordLogin(ctx, db, user, "10.0.0.1", false); err == nil { t.Fatal("banned user must fail") } other := &models.User{Email: "bea@example.test", Password: "x"} if err := db.Create(other).Error; err != nil { t.Fatal(err) } if err := CheckAndRecordLogin(ctx, db, other, "10.0.0.2", false); err != nil { t.Fatal(err) } if err := CheckAndRecordLogin(ctx, db, other, "10.0.0.2", true); err != nil { t.Fatal(err) } var cleared models.Throttle if err := db.Where("user_id = ?", other.ID).Take(&cleared).Error; err != nil { t.Fatal(err) } if cleared.Attempts != 0 || cleared.IsSuspended { t.Fatalf("reset = %+v", cleared) } before := countThrottle(t, db) if err := CheckAndRecordLogin(ctx, db, nil, "10.0.0.9", false); err != nil { t.Fatal(err) } if countThrottle(t, db) != before { t.Fatal("nil user created a throttle row") } } func countThrottle(t *testing.T, db *gorm.DB) int64 { t.Helper() var n int64 if err := db.Model(&models.Throttle{}).Count(&n).Error; err != nil { t.Fatal(err) } return n } func throttleDB(t *testing.T) *gorm.DB { t.Helper() ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute) t.Cleanup(cancel) ctr, err := postgres.Run(ctx, "postgres:16-alpine", postgres.WithDatabase("throttle"), postgres.WithUsername("throttle"), postgres.WithPassword("throttle"), postgres.BasicWaitStrategies(), testcontainers.WithEnv(map[string]string{ "POSTGRES_INITDB_ARGS": "--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8", }), ) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = testcontainers.TerminateContainer(ctr) }) dsn, err := ctr.ConnectionString(ctx, "sslmode=disable") if err != nil { t.Fatal(err) } sqlDB, err := sql.Open("pgx", dsn) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = sqlDB.Close() }) gdb, err := lagoon.Use(ctx, sqlDB) if err != nil { t.Fatal(err) } m := gormigrate.New(gdb, &gormigrate.Options{ TableName: "summer_migrations_golem15_user", UseTransaction: true, }, updates.All()) if err := m.Migrate(); err != nil { t.Fatal(err) } return gdb }