Files
Nip-05-api/internal/payments/worker_test.go
2026-04-29 02:35:00 +00:00

181 lines
5.0 KiB
Go

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: &current}
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")
}
}