205 lines
5.9 KiB
Go
205 lines
5.9 KiB
Go
package expiry
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/noderunners/nip05api/internal/audit"
|
|
"github.com/noderunners/nip05api/internal/db"
|
|
"github.com/noderunners/nip05api/internal/dm"
|
|
"github.com/noderunners/nip05api/internal/messages"
|
|
"github.com/noderunners/nip05api/internal/user"
|
|
"github.com/noderunners/nip05api/internal/webhook"
|
|
)
|
|
|
|
const testHex = "0e8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c"
|
|
|
|
func newTestStack(t *testing.T) (*db.DB, *user.Service, *dm.Service, *webhook.Service, *audit.Logger) {
|
|
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() })
|
|
|
|
tmpls, _ := messages.Load("/nonexistent.yaml")
|
|
users := user.NewService(user.NewRepo(d), nil)
|
|
dms := dm.NewService(dm.NewRepo(d), tmpls, true)
|
|
hooks := webhook.NewService(webhook.NewRepo(d), "test.local", true)
|
|
return d, users, dms, hooks, audit.New(d)
|
|
}
|
|
|
|
func TestExpiry_Reminder(t *testing.T) {
|
|
d, users, dms, hooks, aud := newTestStack(t)
|
|
now := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC)
|
|
expires := now.Add(7 * 24 * time.Hour)
|
|
|
|
u := &user.User{
|
|
Pubkey: testHex,
|
|
Username: "alice",
|
|
SubscriptionType: user.SubYearly,
|
|
ExpiresAt: &expires,
|
|
IsActive: true,
|
|
}
|
|
if err := users.Repo().Insert(context.Background(), u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
w := NewWorker(users, dms, hooks, aud, "test.local", "https://test.local",
|
|
30, []int{7}, 9).WithClock(func() time.Time { return now })
|
|
w.RunOnce(context.Background())
|
|
|
|
var dmCount int
|
|
if err := d.QueryRowContext(context.Background(),
|
|
`SELECT COUNT(*) FROM dm_outbox WHERE event_type = 'expiring_soon'`).Scan(&dmCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dmCount != 1 {
|
|
t.Errorf("expected 1 expiring_soon DM, got %d", dmCount)
|
|
}
|
|
|
|
// Reminder flag should be set, so a re-run does not double-send.
|
|
w.RunOnce(context.Background())
|
|
if err := d.QueryRowContext(context.Background(),
|
|
`SELECT COUNT(*) FROM dm_outbox WHERE event_type = 'expiring_soon'`).Scan(&dmCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dmCount != 1 {
|
|
t.Errorf("re-run should not double-fire, got %d", dmCount)
|
|
}
|
|
}
|
|
|
|
func TestExpiry_Expiration(t *testing.T) {
|
|
d, users, dms, hooks, aud := newTestStack(t)
|
|
now := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC)
|
|
expired := now.Add(-1 * time.Hour)
|
|
|
|
u := &user.User{
|
|
Pubkey: testHex,
|
|
Username: "bob",
|
|
SubscriptionType: user.SubYearly,
|
|
ExpiresAt: &expired,
|
|
IsActive: true,
|
|
}
|
|
if err := users.Repo().Insert(context.Background(), u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
w := NewWorker(users, dms, hooks, aud, "test.local", "https://test.local",
|
|
30, []int{7}, 9).WithClock(func() time.Time { return now })
|
|
w.RunOnce(context.Background())
|
|
|
|
got, err := users.Repo().GetByPubkey(context.Background(), testHex)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.IsActive {
|
|
t.Error("expected user deactivated")
|
|
}
|
|
if got.DeactivatedAt == nil {
|
|
t.Error("expected deactivated_at set")
|
|
}
|
|
|
|
var dmCount int
|
|
if err := d.QueryRowContext(context.Background(),
|
|
`SELECT COUNT(*) FROM dm_outbox WHERE event_type = 'expired'`).Scan(&dmCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dmCount != 1 {
|
|
t.Errorf("expected 1 expired DM, got %d", dmCount)
|
|
}
|
|
|
|
var hookCount int
|
|
if err := d.QueryRowContext(context.Background(),
|
|
`SELECT COUNT(*) FROM webhook_outbox WHERE event_type = 'user.removed'`).Scan(&hookCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hookCount != 1 {
|
|
t.Errorf("expected 1 user.removed webhook, got %d", hookCount)
|
|
}
|
|
}
|
|
|
|
func TestExpiry_GraceCleanup(t *testing.T) {
|
|
d, users, dms, hooks, aud := newTestStack(t)
|
|
now := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC)
|
|
deactivated := now.Add(-31 * 24 * time.Hour)
|
|
expired := now.Add(-32 * 24 * time.Hour)
|
|
|
|
u := &user.User{
|
|
Pubkey: testHex,
|
|
Username: "charlie",
|
|
SubscriptionType: user.SubYearly,
|
|
ExpiresAt: &expired,
|
|
IsActive: false,
|
|
DeactivatedAt: &deactivated,
|
|
}
|
|
if err := users.Repo().Insert(context.Background(), u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Insert sets is_active=1 by default; force it inactive.
|
|
if err := users.Repo().Update(context.Background(), u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
w := NewWorker(users, dms, hooks, aud, "test.local", "https://test.local",
|
|
30, []int{7}, 9).WithClock(func() time.Time { return now })
|
|
w.RunOnce(context.Background())
|
|
|
|
// User should be hard-deleted.
|
|
_, err := users.Repo().GetByPubkey(context.Background(), testHex)
|
|
if err == nil {
|
|
t.Error("expected user deleted after grace cleanup")
|
|
}
|
|
|
|
// Audit log should record the purge.
|
|
var auditCount int
|
|
if err := d.QueryRowContext(context.Background(),
|
|
`SELECT COUNT(*) FROM audit_log WHERE action = 'user.grace_purged'`).Scan(&auditCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if auditCount != 1 {
|
|
t.Errorf("expected 1 grace_purged audit, got %d", auditCount)
|
|
}
|
|
}
|
|
|
|
func TestExpiry_LifetimeUserNotAffected(t *testing.T) {
|
|
d, users, dms, hooks, aud := newTestStack(t)
|
|
now := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC)
|
|
|
|
u := &user.User{
|
|
Pubkey: testHex,
|
|
Username: "lifetime",
|
|
SubscriptionType: user.SubLifetime,
|
|
IsActive: true,
|
|
}
|
|
if err := users.Repo().Insert(context.Background(), u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
w := NewWorker(users, dms, hooks, aud, "test.local", "https://test.local",
|
|
30, []int{7}, 9).WithClock(func() time.Time { return now })
|
|
w.RunOnce(context.Background())
|
|
|
|
got, err := users.Repo().GetByPubkey(context.Background(), testHex)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !got.IsActive {
|
|
t.Error("lifetime user should not be deactivated")
|
|
}
|
|
|
|
var dmCount int
|
|
if err := d.QueryRowContext(context.Background(),
|
|
`SELECT COUNT(*) FROM dm_outbox`).Scan(&dmCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dmCount != 0 {
|
|
t.Errorf("lifetime user should receive no DM, got %d", dmCount)
|
|
}
|
|
}
|