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

228 lines
6.3 KiB
Go

package invoice_test
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/noderunners/nip05api/internal/db"
"github.com/noderunners/nip05api/internal/invoice"
"github.com/noderunners/nip05api/internal/user"
)
const testHex = "0e8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c"
func newServiceFixture(t *testing.T) (*invoice.Service, *user.Service, *httptest.Server) {
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() })
ln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{
"payment_hash": "hash-" + r.URL.Path,
"payment_request": "lnbcfake",
})
}))
t.Cleanup(ln.Close)
users := user.NewService(user.NewRepo(d), []string{"admin", "root"})
client := invoice.NewLNbits(ln.URL, "key")
pricing := invoice.Pricing{YearlySats: 1000, LifetimeSats: 5000, ExpiryMins: 30}
svc := invoice.NewService(invoice.NewRepo(d), users, client, pricing, "test.local")
return svc, users, ln
}
func TestCreate_PubkeyOnlyDefaultsLifetime(t *testing.T) {
svc, _, _ := newServiceFixture(t)
p, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
SubscriptionType: user.SubLifetime,
})
if err != nil {
t.Fatalf("create: %v", err)
}
if p.SubscriptionType != user.SubLifetime {
t.Errorf("expected lifetime, got %q", p.SubscriptionType)
}
if p.AmountSats != 5000 {
t.Errorf("expected lifetime price, got %d", p.AmountSats)
}
if !strings.HasPrefix(p.Username, "u_") {
t.Errorf("expected provisional username, got %q", p.Username)
}
if err := user.ValidateUsername(p.Username, nil); err != nil {
t.Errorf("provisional name should be valid: %v", err)
}
}
func TestCreate_YearlyDefaultsOneYear(t *testing.T) {
svc, _, _ := newServiceFixture(t)
p, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
Username: "alice",
SubscriptionType: user.SubYearly,
Years: 1,
})
if err != nil {
t.Fatalf("create: %v", err)
}
if p.Years != 1 || p.AmountSats != 1000 {
t.Errorf("unexpected pending: years=%d amount=%d", p.Years, p.AmountSats)
}
if p.Username != "alice" {
t.Errorf("expected explicit username, got %q", p.Username)
}
}
func TestCreate_RenewalReusesUsername(t *testing.T) {
svc, users, _ := newServiceFixture(t)
if _, err := users.CreateOrActivate(context.Background(), testHex, "alice", user.SubYearly, 1, false); err != nil {
t.Fatal(err)
}
p, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
SubscriptionType: user.SubLifetime,
})
if err != nil {
t.Fatalf("create: %v", err)
}
if !p.IsRenewal {
t.Error("expected renewal flag")
}
if p.Username != "alice" {
t.Errorf("expected stored username, got %q", p.Username)
}
}
func TestCreate_BlocksActiveLifetimeUser(t *testing.T) {
svc, users, _ := newServiceFixture(t)
if _, err := users.CreateOrActivate(context.Background(), testHex, "alice", user.SubLifetime, 0, false); err != nil {
t.Fatal(err)
}
_, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
SubscriptionType: user.SubYearly,
Years: 1,
})
if !errors.Is(err, invoice.ErrLifetimeAccess) {
t.Fatalf("expected ErrLifetimeAccess, got %v", err)
}
}
func TestCreate_RejectsDuplicatePending(t *testing.T) {
svc, _, _ := newServiceFixture(t)
if _, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
Username: "alice",
SubscriptionType: user.SubYearly,
Years: 1,
}); err != nil {
t.Fatalf("first create: %v", err)
}
_, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
Username: "alice",
SubscriptionType: user.SubYearly,
Years: 1,
})
if !errors.Is(err, invoice.ErrPendingInvoiceExists) {
t.Fatalf("expected ErrPendingInvoiceExists, got %v", err)
}
}
func TestCreate_YearlyPersistsTargetExpiry(t *testing.T) {
svc, users, _ := newServiceFixture(t)
if _, err := users.CreateOrActivate(context.Background(), testHex, "alice", user.SubYearly, 1, false); err != nil {
t.Fatal(err)
}
existing, err := users.Repo().GetByPubkey(context.Background(), testHex)
if err != nil {
t.Fatal(err)
}
before := time.Now().UTC()
p, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
SubscriptionType: user.SubYearly,
Years: 1,
})
if err != nil {
t.Fatalf("create: %v", err)
}
stored, err := svc.Repo().Get(context.Background(), p.PaymentHash)
if err != nil {
t.Fatal(err)
}
if !stored.TargetSet || stored.TargetExpiresAt == nil {
t.Fatalf("expected target persisted, got set=%v at=%v", stored.TargetSet, stored.TargetExpiresAt)
}
want := existing.ExpiresAt.AddDate(1, 0, 0)
if !stored.TargetExpiresAt.Equal(want) {
t.Errorf("target stacking mismatch: got %v want %v", stored.TargetExpiresAt, want)
}
if stored.TargetExpiresAt.Before(before) {
t.Error("target should be in the future")
}
}
func TestCreate_YearlyExpiredUserStartsFromNow(t *testing.T) {
svc, users, _ := newServiceFixture(t)
if _, err := users.CreateOrActivate(context.Background(), testHex, "alice", user.SubYearly, 1, false); err != nil {
t.Fatal(err)
}
u, err := users.Repo().GetByPubkey(context.Background(), testHex)
if err != nil {
t.Fatal(err)
}
past := time.Now().UTC().AddDate(0, 0, -1)
u.ExpiresAt = &past
if err := users.Repo().Update(context.Background(), u); err != nil {
t.Fatal(err)
}
before := time.Now().UTC()
p, err := svc.Create(context.Background(), invoice.CreateRequest{
Pubkey: testHex,
SubscriptionType: user.SubYearly,
Years: 1,
})
if err != nil {
t.Fatalf("create: %v", err)
}
stored, err := svc.Repo().Get(context.Background(), p.PaymentHash)
if err != nil {
t.Fatal(err)
}
if stored.TargetExpiresAt == nil {
t.Fatal("expected target")
}
want := before.AddDate(1, 0, 0)
delta := stored.TargetExpiresAt.Sub(want)
if delta < -2*time.Second || delta > 2*time.Second {
t.Errorf("expected ~now+1y (%v), got %v", want, stored.TargetExpiresAt)
}
}