package nodes_test import ( "context" "database/sql" "testing" "time" "github.com/wangjia/pangolin/server/internal/config" "github.com/wangjia/pangolin/server/internal/nodes" "github.com/wangjia/pangolin/server/internal/store" ) // -------------------------------------------------------------------------- // RenewCredential is the DB half of "renew-on-activity" (the fix for a paid // session silently dropping ~1 day in — the connect credential's 24h TTL with // nothing refreshing it while a persistent macOS sysext tunnel outlives the GUI // app). Pure-Go modernc sqlite → runs in the default no-docker path alongside // entitlement_override_sqlite_test.go. Exercises the actual UPDATE SQL (column // names, the expires_at > now guard) the handler mock can't cover. // -------------------------------------------------------------------------- func openRenewTestDB(t *testing.T) *sql.DB { t.Helper() db, err := store.Open(&config.Config{Driver: "sqlite", DSN: ":memory:"}) if err != nil { t.Fatalf("store.Open: %v", err) } t.Cleanup(func() { db.Close() }) if err := store.MigrateUp(db, "sqlite"); err != nil { t.Fatalf("MigrateUp: %v", err) } return db } // seedRenewNode inserts a provider + node (provider_id is a FK) and returns the // node id, on which credentials can be hung. func seedRenewNode(t *testing.T, db *sql.DB) int64 { t.Helper() ctx := context.Background() res, err := db.ExecContext(ctx, `INSERT INTO providers (name, api_kind, regions, pool) VALUES ('renew-test', 'vultr', '["HK"]', 'consumable')`) if err != nil { t.Fatalf("seed provider: %v", err) } providerID, err := res.LastInsertId() if err != nil { t.Fatalf("provider id: %v", err) } res, err = db.ExecContext(ctx, ` INSERT INTO nodes (uuid, region, name_zh, name_en, tier, endpoint, reality_pbk, reality_sni, provider_id, status) VALUES ('renew-node', 'HK', '香港', 'HK', 'pro', '1.2.3.4:443', 'pbk', 'sni.example.com', ?, 'up') `, providerID) if err != nil { t.Fatalf("seed node: %v", err) } nodeID, err := res.LastInsertId() if err != nil { t.Fatalf("node id: %v", err) } return nodeID } func insertRenewCred(t *testing.T, db *sql.DB, nodeID int64, dpUUID string, expiresAt time.Time) { t.Helper() if _, err := db.ExecContext(context.Background(), `INSERT INTO connect_credentials (node_id, dp_uuid, protocol, flow, expires_at) VALUES (?, ?, 3, 'xtls-rprx-vision', ?)`, nodeID, dpUUID, expiresAt.UTC()); err != nil { t.Fatalf("insert credential %s: %v", dpUUID, err) } } func readRenewExpiry(t *testing.T, db *sql.DB, dpUUID string) time.Time { t.Helper() var exp time.Time if err := db.QueryRowContext(context.Background(), `SELECT expires_at FROM connect_credentials WHERE dp_uuid = ?`, dpUUID).Scan(&exp); err != nil { t.Fatalf("read expiry %s: %v", dpUUID, err) } return exp.UTC() } // TestRenewCredential_BumpsActive: a still-active credential's expiry is pushed // forward to the requested time. func TestRenewCredential_BumpsActive(t *testing.T) { db := openRenewTestDB(t) st := nodes.NewSQLNodeStore(db) nodeID := seedRenewNode(t, db) now := time.Now().UTC() insertRenewCred(t, db, nodeID, "dp-active", now.Add(1*time.Hour)) // still active newExp := now.Add(nodes.PaidCredentialTTL) if err := st.RenewCredential(context.Background(), "dp-active", newExp); err != nil { t.Fatalf("RenewCredential: %v", err) } got := readRenewExpiry(t, db, "dp-active") if got.Sub(newExp).Abs() > time.Second { t.Errorf("expiry = %v, want ~%v (renewed)", got, newExp) } } // TestRenewCredential_LeavesExpired: an already-expired credential is NOT // resurrected (the expires_at > now guard) — a disconnected session must go // through a fresh /connect, not get silently revived by a late usage report. func TestRenewCredential_LeavesExpired(t *testing.T) { db := openRenewTestDB(t) st := nodes.NewSQLNodeStore(db) nodeID := seedRenewNode(t, db) now := time.Now().UTC() expiredAt := now.Add(-1 * time.Hour) insertRenewCred(t, db, nodeID, "dp-expired", expiredAt) // already expired if err := st.RenewCredential(context.Background(), "dp-expired", now.Add(nodes.PaidCredentialTTL)); err != nil { t.Fatalf("RenewCredential: %v", err) } got := readRenewExpiry(t, db, "dp-expired") if got.After(now) { t.Errorf("expiry = %v, want unchanged (still expired, before %v)", got, now) } }