package card import ( "context" "testing" "gpt-plus/internal/db" "github.com/glebarez/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func setupCardTestDB(t *testing.T) *gorm.DB { t.Helper() d, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("open db: %v", err) } d.AutoMigrate(&db.SystemConfig{}, &db.Card{}, &db.CardCode{}) db.DB = d return d } func insertCard(t *testing.T, d *gorm.DB, number, status string, maxBinds int) *db.Card { t.Helper() card := &db.Card{ NumberHash: db.HashSHA256(number), NumberEnc: number, // no encryption in test (key not set) CVCEnc: "123", ExpMonth: "12", ExpYear: "2030", Name: "Test", Country: "US", Status: status, MaxBinds: maxBinds, } d.Create(card) return card } func TestGetCardReturnsActiveCard(t *testing.T) { d := setupCardTestDB(t) insertCard(t, d, "4111111111111111", "active", 3) prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) card, err := prov.GetCard(context.Background()) if err != nil { t.Fatalf("GetCard: %v", err) } if card.Number != "4111111111111111" { t.Fatalf("number = %q", card.Number) } } func TestGetCardActivatesAvailable(t *testing.T) { d := setupCardTestDB(t) insertCard(t, d, "4242424242424242", "available", 1) prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) card, err := prov.GetCard(context.Background()) if err != nil { t.Fatalf("GetCard: %v", err) } if card.Number != "4242424242424242" { t.Fatalf("number = %q", card.Number) } var dbCard db.Card d.First(&dbCard, "number_hash = ?", db.HashSHA256("4242424242424242")) if dbCard.Status != "active" { t.Fatalf("card should now be active, got %q", dbCard.Status) } } func TestGetCardNoCardsReturnsError(t *testing.T) { d := setupCardTestDB(t) prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) _, err := prov.GetCard(context.Background()) if err == nil { t.Fatal("expected error when no cards") } } func TestReportResultSuccessIncrementsBind(t *testing.T) { d := setupCardTestDB(t) insertCard(t, d, "5555555555554444", "active", 3) prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) card, _ := prov.GetCard(context.Background()) err := prov.ReportResult(context.Background(), card, true) if err != nil { t.Fatalf("ReportResult: %v", err) } var dbCard db.Card d.First(&dbCard, "number_hash = ?", db.HashSHA256("5555555555554444")) if dbCard.BindCount != 1 { t.Fatalf("bind_count = %d, want 1", dbCard.BindCount) } if dbCard.Status != "active" { t.Fatalf("still active when under max_binds, got %q", dbCard.Status) } } func TestReportResultExhaustsCard(t *testing.T) { d := setupCardTestDB(t) c := insertCard(t, d, "6011111111111117", "active", 1) // Pre-set to 0, so one success = 1 = maxBinds → exhausted _ = c prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) card, _ := prov.GetCard(context.Background()) prov.ReportResult(context.Background(), card, true) var dbCard db.Card d.First(&dbCard, "number_hash = ?", db.HashSHA256("6011111111111117")) if dbCard.Status != "exhausted" { t.Fatalf("status = %q, want exhausted", dbCard.Status) } } func TestReportResultExhaustedActivatesNext(t *testing.T) { d := setupCardTestDB(t) insertCard(t, d, "1111000011110000", "active", 1) insertCard(t, d, "2222000022220000", "available", 5) prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) card, _ := prov.GetCard(context.Background()) prov.ReportResult(context.Background(), card, true) // exhaust first // Now GetCard should return the second card next, err := prov.GetCard(context.Background()) if err != nil { t.Fatalf("GetCard after exhaust: %v", err) } if next.Number != "2222000022220000" { t.Fatalf("next card = %q, want 2222000022220000", next.Number) } } func TestReportResultRejectedCard(t *testing.T) { d := setupCardTestDB(t) insertCard(t, d, "3333000033330000", "active", 5) prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) card, _ := prov.GetCard(context.Background()) prov.ReportResult(context.Background(), card, false) var dbCard db.Card d.First(&dbCard, "number_hash = ?", db.HashSHA256("3333000033330000")) if dbCard.Status != "rejected" { t.Fatalf("status = %q, want rejected", dbCard.Status) } if dbCard.LastError == "" { t.Fatal("last_error should be set") } } func TestDefaultCountryAndCurrency(t *testing.T) { prov := NewDBCardProvider(DBCardProviderConfig{}) if prov.defaultCountry != "US" { t.Fatalf("defaultCountry = %q, want US", prov.defaultCountry) } if prov.defaultCurrency != "USD" { t.Fatalf("defaultCurrency = %q, want USD", prov.defaultCurrency) } } func TestToCardInfoAppliesDefaults(t *testing.T) { d := setupCardTestDB(t) prov := NewDBCardProvider(DBCardProviderConfig{ DB: d, DefaultAddress: "123 Main St", DefaultCity: "NYC", DefaultState: "NY", DefaultPostalCode: "10001", }) card := &db.Card{ NumberEnc: "4111111111111111", CVCEnc: "123", ExpMonth: "12", ExpYear: "2030", Name: "Test", Country: "US", } info, err := prov.toCardInfo(card) if err != nil { t.Fatalf("toCardInfo: %v", err) } if info.Address != "123 Main St" { t.Fatalf("address = %q, want default", info.Address) } if info.City != "NYC" || info.State != "NY" || info.PostalCode != "10001" { t.Fatalf("defaults not applied: %+v", info) } } func TestSingleActiveCardPolicy(t *testing.T) { d := setupCardTestDB(t) insertCard(t, d, "AAAA000000001111", "active", 5) insertCard(t, d, "BBBB000000002222", "available", 5) // Verify only one active var count int64 d.Model(&db.Card{}).Where("status = ?", "active").Count(&count) if count != 1 { t.Fatalf("active count = %d, want 1", count) } prov := NewDBCardProvider(DBCardProviderConfig{DB: d}) card, _ := prov.GetCard(context.Background()) if card.Number != "AAAA000000001111" { t.Fatalf("should return the active card, got %q", card.Number) } }