181 lines
5.0 KiB
Go
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: ¤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")
|
|
}
|
|
}
|