package payments import ( "context" "path/filepath" "testing" "time" "github.com/noderunners/nip05api/internal/db" "github.com/noderunners/nip05api/internal/invoice" "github.com/noderunners/nip05api/internal/user" ) const testHex = "0e8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c" func newTestDB(t *testing.T) *db.DB { t.Helper() d, err := db.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatal(err) } if err := d.Migrate(context.Background()); err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) return d } func TestComputeTarget_NewYearly(t *testing.T) { now := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) p := &invoice.PendingInvoice{SubscriptionType: user.SubYearly, Years: 1} got := computeTarget(p, nil, now) if got == nil || !got.Equal(now.AddDate(1, 0, 0)) { t.Errorf("got %v want %v", got, now.AddDate(1, 0, 0)) } } func TestComputeTarget_RenewActive(t *testing.T) { now := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) current := now.AddDate(0, 6, 0) p := &invoice.PendingInvoice{SubscriptionType: user.SubYearly, Years: 1} existing := &user.User{ExpiresAt: ¤t} got := computeTarget(p, existing, now) want := current.AddDate(1, 0, 0) if !got.Equal(want) { t.Errorf("got %v want %v", got, want) } } func TestComputeTarget_RenewExpired(t *testing.T) { now := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) past := now.AddDate(0, -1, 0) p := &invoice.PendingInvoice{SubscriptionType: user.SubYearly, Years: 1} existing := &user.User{ExpiresAt: &past} got := computeTarget(p, existing, now) want := now.AddDate(1, 0, 0) if !got.Equal(want) { t.Errorf("got %v want %v", got, want) } } func TestComputeTarget_Lifetime(t *testing.T) { now := time.Now() p := &invoice.PendingInvoice{SubscriptionType: user.SubLifetime} if got := computeTarget(p, nil, now); got != nil { t.Errorf("expected nil, got %v", got) } } // Idempotent confirm: applying the same target twice produces identical state. func TestSetActiveExpiry_Idempotent(t *testing.T) { d := newTestDB(t) repo := user.NewRepo(d) expires := time.Date(2027, 6, 1, 0, 0, 0, 0, time.UTC) u := &user.User{ Pubkey: testHex, Username: "alice", SubscriptionType: user.SubYearly, ExpiresAt: &expires, IsActive: true, } if err := repo.Insert(context.Background(), u); err != nil { t.Fatal(err) } target := time.Date(2028, 6, 1, 0, 0, 0, 0, time.UTC) if err := repo.SetActiveExpiry(context.Background(), testHex, user.SubYearly, &target); err != nil { t.Fatal(err) } got, err := repo.GetByPubkey(context.Background(), testHex) if err != nil { t.Fatal(err) } if !got.ExpiresAt.Equal(target) { t.Errorf("first apply: got %v want %v", got.ExpiresAt, target) } // Re-apply same target — must not advance further. if err := repo.SetActiveExpiry(context.Background(), testHex, user.SubYearly, &target); err != nil { t.Fatal(err) } got, _ = repo.GetByPubkey(context.Background(), testHex) if !got.ExpiresAt.Equal(target) { t.Errorf("re-apply changed value: got %v want %v", got.ExpiresAt, target) } } func TestSetTargetIfUnset_OnlyOnce(t *testing.T) { d := newTestDB(t) repo := invoice.NewRepo(d) target1 := time.Date(2027, 6, 1, 0, 0, 0, 0, time.UTC) p := &invoice.PendingInvoice{ PaymentHash: "hash1", PaymentRequest: "lnbc1...", Username: "alice", Pubkey: testHex, SubscriptionType: user.SubYearly, Years: 1, AmountSats: 1000, ExpiresAt: time.Now().Add(30 * time.Minute), } if err := repo.Insert(context.Background(), p); err != nil { t.Fatal(err) } won, err := repo.SetTargetIfUnset(context.Background(), "hash1", &target1) if err != nil || !won { t.Fatalf("first set: won=%v err=%v", won, err) } target2 := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC) won, err = repo.SetTargetIfUnset(context.Background(), "hash1", &target2) if err != nil { t.Fatal(err) } if won { t.Error("second set should be a no-op") } fresh, err := repo.Get(context.Background(), "hash1") if err != nil { t.Fatal(err) } if !fresh.TargetSet || fresh.TargetExpiresAt == nil || !fresh.TargetExpiresAt.Equal(target1) { t.Errorf("target should be the first value, got set=%v at=%v", fresh.TargetSet, fresh.TargetExpiresAt) } } func TestClaimPaid_Atomic(t *testing.T) { d := newTestDB(t) repo := invoice.NewRepo(d) p := &invoice.PendingInvoice{ PaymentHash: "hash2", PaymentRequest: "lnbc...", Username: "bob", Pubkey: testHex, SubscriptionType: user.SubYearly, Years: 1, AmountSats: 1000, ExpiresAt: time.Now().Add(30 * time.Minute), } if err := repo.Insert(context.Background(), p); err != nil { t.Fatal(err) } won1, err := repo.ClaimPaid(context.Background(), "hash2") if err != nil || !won1 { t.Fatalf("first claim: won=%v err=%v", won1, err) } won2, err := repo.ClaimPaid(context.Background(), "hash2") if err != nil { t.Fatal(err) } if won2 { t.Error("second claim should lose race") } }