package auth import ( "context" "database/sql" "testing" "time" _ "github.com/tursodatabase/go-libsql" "github.com/felt-app/felt/internal/store" ) func setupTestDB(t *testing.T) *sql.DB { t.Helper() tmpDir := t.TempDir() db, err := store.Open(tmpDir, true) if err != nil { t.Fatalf("open database: %v", err) } t.Cleanup(func() { db.Close() }) return db.DB } func setupTestAuth(t *testing.T) (*AuthService, *sql.DB) { t.Helper() db := setupTestDB(t) jwtService := NewJWTService([]byte("test-signing-key-32-bytes-long!!"), 7*24*time.Hour) authService := NewAuthService(db, jwtService) return authService, db } func TestSuccessfulLogin(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() // Dev seed creates admin with PIN 1234 token, operator, err := authService.Login(ctx, "1234") if err != nil { t.Fatalf("login error: %v", err) } if token == "" { t.Fatal("expected non-empty token") } if operator.Name != "Admin" { t.Fatalf("expected operator name Admin, got %s", operator.Name) } if operator.Role != "admin" { t.Fatalf("expected role admin, got %s", operator.Role) } } func TestLoginReturnsValidJWT(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() token, _, err := authService.Login(ctx, "1234") if err != nil { t.Fatalf("login error: %v", err) } // Validate the returned token jwtService := NewJWTService([]byte("test-signing-key-32-bytes-long!!"), 7*24*time.Hour) claims, err := jwtService.ValidateToken(token) if err != nil { t.Fatalf("validate token error: %v", err) } if claims.OperatorID == "" { t.Fatal("expected non-empty operator ID in claims") } if claims.Role != "admin" { t.Fatalf("expected role admin in claims, got %s", claims.Role) } } func TestWrongPINReturnsError(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() _, _, err := authService.Login(ctx, "9999") if err != ErrInvalidPIN { t.Fatalf("expected ErrInvalidPIN, got %v", err) } } func TestEmptyPINReturnsError(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() _, _, err := authService.Login(ctx, "") if err != ErrInvalidPIN { t.Fatalf("expected ErrInvalidPIN, got %v", err) } } func TestRateLimitingAfter5Failures(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() // Make 5 failed attempts for i := 0; i < 5; i++ { _, _, err := authService.Login(ctx, "wrong") if err == nil { t.Fatal("expected error for wrong PIN") } } // Check global failure count count, err := authService.GetFailureCount(ctx, "_global") if err != nil { t.Fatalf("get failure count: %v", err) } if count < 5 { t.Fatalf("expected at least 5 failures, got %d", count) } // 6th attempt should trigger rate limiting _, _, err = authService.Login(ctx, "wrong") if err != ErrTooManyAttempts && err != ErrInvalidPIN { // Rate limiting may or may not kick in on the exact boundary, // but the failure count should be tracked t.Logf("6th attempt error: %v (rate limiting may be delayed)", err) } } func TestSuccessfulLoginResetsFailureCounter(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() // Make 3 failed attempts (below lockout threshold) for i := 0; i < 3; i++ { authService.Login(ctx, "wrong") } count, _ := authService.GetFailureCount(ctx, "_global") if count != 3 { t.Fatalf("expected 3 failures, got %d", count) } // Successful login resets counter _, _, err := authService.Login(ctx, "1234") if err != nil { t.Fatalf("login error: %v", err) } count, _ = authService.GetFailureCount(ctx, "_global") if count != 0 { t.Fatalf("expected 0 failures after successful login, got %d", count) } } func TestJWTValidationWithExpiredToken(t *testing.T) { signingKey := []byte("test-signing-key-32-bytes-long!!") jwtService := NewJWTService(signingKey, -time.Hour) // Already expired token, err := jwtService.NewToken("op-1", "admin") if err != nil { t.Fatalf("create token: %v", err) } _, err = jwtService.ValidateToken(token) if err == nil { t.Fatal("expected error for expired token") } } func TestJWTValidationEnforcesHS256(t *testing.T) { signingKey := []byte("test-signing-key-32-bytes-long!!") jwtService := NewJWTService(signingKey, time.Hour) // Valid token should pass token, err := jwtService.NewToken("op-1", "admin") if err != nil { t.Fatalf("create token: %v", err) } claims, err := jwtService.ValidateToken(token) if err != nil { t.Fatalf("validate valid token: %v", err) } if claims.OperatorID != "op-1" { t.Fatalf("expected operator ID op-1, got %s", claims.OperatorID) } } func TestHashPIN(t *testing.T) { hash, err := HashPIN("1234") if err != nil { t.Fatalf("hash PIN: %v", err) } if hash == "" { t.Fatal("expected non-empty hash") } if hash == "1234" { t.Fatal("hash should not equal plaintext PIN") } } func TestCreateAndListOperators(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() // Create a new operator op, err := authService.CreateOperator(ctx, "Floor Staff", "5678", "floor") if err != nil { t.Fatalf("create operator: %v", err) } if op.Name != "Floor Staff" { t.Fatalf("expected name Floor Staff, got %s", op.Name) } if op.Role != "floor" { t.Fatalf("expected role floor, got %s", op.Role) } // List operators (should include dev seed admin + new operator) operators, err := authService.ListOperators(ctx) if err != nil { t.Fatalf("list operators: %v", err) } if len(operators) < 2 { t.Fatalf("expected at least 2 operators, got %d", len(operators)) } // Verify we can login with the new operator's PIN token, loginOp, err := authService.Login(ctx, "5678") if err != nil { t.Fatalf("login with new operator: %v", err) } if token == "" { t.Fatal("expected non-empty token") } if loginOp.Role != "floor" { t.Fatalf("expected role floor, got %s", loginOp.Role) } } func TestCreateOperatorValidation(t *testing.T) { authService, _ := setupTestAuth(t) ctx := context.Background() // Empty name _, err := authService.CreateOperator(ctx, "", "1234", "admin") if err == nil { t.Fatal("expected error for empty name") } // Empty PIN _, err = authService.CreateOperator(ctx, "Test", "", "admin") if err == nil { t.Fatal("expected error for empty PIN") } // Invalid role _, err = authService.CreateOperator(ctx, "Test", "1234", "superadmin") if err == nil { t.Fatal("expected error for invalid role") } } func TestLoadOrCreateSigningKey(t *testing.T) { db := setupTestDB(t) // First call generates a new key key1, err := LoadOrCreateSigningKey(db) if err != nil { t.Fatalf("first load: %v", err) } if len(key1) != 32 { t.Fatalf("expected 32-byte key, got %d bytes", len(key1)) } // Second call loads the same key key2, err := LoadOrCreateSigningKey(db) if err != nil { t.Fatalf("second load: %v", err) } if string(key1) != string(key2) { t.Fatal("expected same key on second load") } }