package watcher import ( "context" "testing" "time" "github.com/wangjia/pangolin/pay/internal/store" "github.com/wangjia/pangolin/pay/internal/tron" ) type mockFetcher struct{ m map[string][]tron.Transfer } func (f *mockFetcher) IncomingTransfers(_ context.Context, addr string) ([]tron.Transfer, error) { return f.m[addr], nil } func newPending(t *testing.T, st *store.Store, orderNo, addr string, amount int64, expires time.Time) { t.Helper() o := &store.Order{ OrderNo: orderNo, SKU: "pro", ExpectAmount: amount, Address: addr, Status: store.StatusPending, CreatedAt: time.Unix(1_700_000_000, 0), ExpiresAt: expires, } if err := st.CreateOrder(context.Background(), o); err != nil { t.Fatalf("seed order: %v", err) } } func TestWatcherMarksPaidOnSufficientTransfer(t *testing.T) { st, _ := store.Open(":memory:") t.Cleanup(func() { _ = st.Close() }) ctx := context.Background() now := time.Unix(1_700_000_100, 0) newPending(t, st, "PAY1", "TADDR1", 5_000000, now.Add(time.Hour)) fetch := &mockFetcher{m: map[string][]tron.Transfer{}} w := New(st, fetch, nil) w.now = func() time.Time { return now } // No transfer yet -> stays pending. if err := w.Tick(ctx); err != nil { t.Fatalf("tick1: %v", err) } if o, _ := st.GetOrder(ctx, "PAY1"); o.Status != store.StatusPending { t.Fatalf("want pending, got %s", o.Status) } // Underpayment -> still pending. fetch.m["TADDR1"] = []tron.Transfer{{TxID: "tx-under", To: "TADDR1", Value: 4_000000}} _ = w.Tick(ctx) if o, _ := st.GetOrder(ctx, "PAY1"); o.Status != store.StatusPending { t.Fatalf("underpay should stay pending, got %s", o.Status) } // Sufficient payment -> paid, tx recorded. fetch.m["TADDR1"] = []tron.Transfer{{TxID: "tx-ok", To: "TADDR1", Value: 5_000000}} _ = w.Tick(ctx) o, _ := st.GetOrder(ctx, "PAY1") if o.Status != store.StatusPaid || o.TxID != "tx-ok" { t.Fatalf("want paid/tx-ok, got %s/%s", o.Status, o.TxID) } // Idempotent: another tick with same transfer doesn't error or flip anything. if err := w.Tick(ctx); err != nil { t.Fatalf("idempotent tick: %v", err) } o, _ = st.GetOrder(ctx, "PAY1") if o.Status != store.StatusPaid || o.TxID != "tx-ok" { t.Fatalf("idempotency broken: %s/%s", o.Status, o.TxID) } } func TestWatcherExpiresOverdue(t *testing.T) { st, _ := store.Open(":memory:") t.Cleanup(func() { _ = st.Close() }) ctx := context.Background() now := time.Unix(1_700_000_100, 0) newPending(t, st, "OLD", "TADDR2", 1_000000, now.Add(-time.Minute)) // already overdue w := New(st, &mockFetcher{m: map[string][]tron.Transfer{}}, nil) w.now = func() time.Time { return now } if err := w.Tick(ctx); err != nil { t.Fatalf("tick: %v", err) } if o, _ := st.GetOrder(ctx, "OLD"); o.Status != store.StatusExpired { t.Fatalf("want expired, got %s", o.Status) } }