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" ) // -------------------------------------------------------------------------- // C1 · EntitlementForUser is the *other* consumer of migration 000025's // users.max_devices_override — the per-connection backstop in // internal/httpapi/nodes.go (DEVICE_LIMIT_EXCEEDED). Before this fix it never // read the override, so an operator-granted override (e.g. 6) would pass the // login gate (internal/devices.ResolvePlan reads it) but still get the // account's *next VPN connection* rejected under the un-overridden plan cap. // // No container needed — modernc.org/sqlite is pure Go — so these run in // normal CI alongside run_sqlite_test.sh's data-layer tests, mirroring // internal/devices/device_limit_override_test.go. // -------------------------------------------------------------------------- func openEntitlementTestDB(t *testing.T) *sql.DB { t.Helper() cfg := &config.Config{Driver: "sqlite", DSN: ":memory:"} db, err := store.Open(cfg) 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 } // insertEntitlementTestUser inserts an active user with an optional // max_devices_override (NULL when override.Valid is false). func insertEntitlementTestUser(t *testing.T, db *sql.DB, email string, override sql.NullInt64) int64 { t.Helper() var ( res sql.Result err error ) if override.Valid { res, err = db.Exec( `INSERT INTO users (uuid, email, pw_hash, dp_uuid, status, max_devices_override) VALUES (?, ?, 'x', ?, 'active', ?)`, "uuid-"+email, email, "dp-"+email, override.Int64) } else { res, err = db.Exec( `INSERT INTO users (uuid, email, pw_hash, dp_uuid, status) VALUES (?, ?, 'x', ?, 'active')`, "uuid-"+email, email, "dp-"+email) } if err != nil { t.Fatalf("insert user %s: %v", email, err) } id, err := res.LastInsertId() if err != nil { t.Fatalf("last insert id: %v", err) } return id } // giveEntitlementTestSubscription inserts an active subscription for userID // on the given seeded plan code (free/pro/team, from migration 000007). func giveEntitlementTestSubscription(t *testing.T, db *sql.DB, userID int64, planCode, source string, expiresAt time.Time) { t.Helper() var planID int64 if err := db.QueryRow(`SELECT id FROM plans WHERE code=?`, planCode).Scan(&planID); err != nil { t.Fatalf("plan lookup %s: %v", planCode, err) } if _, err := db.Exec( `INSERT INTO subscriptions (user_id, plan_id, expires_at, source) VALUES (?, ?, ?, ?)`, userID, planID, expiresAt.UTC(), source); err != nil { t.Fatalf("give subscription: %v", err) } } // TestEntitlementForUser_FreeUserNoOverride: a free user with no override set // gets the plan's hard-coded free default (1) — unchanged baseline behavior. func TestEntitlementForUser_FreeUserNoOverride(t *testing.T) { db := openEntitlementTestDB(t) st := nodes.NewSQLNodeStore(db) userID := insertEntitlementTestUser(t, db, "free-none@example.com", sql.NullInt64{}) ent, err := st.EntitlementForUser(context.Background(), userID) if err != nil { t.Fatalf("EntitlementForUser: %v", err) } if ent == nil { t.Fatal("ent = nil, want entitlement") } if ent.PlanCode != "free" { t.Errorf("PlanCode = %q, want free", ent.PlanCode) } if ent.MaxDevices != 1 { t.Errorf("MaxDevices = %d, want 1 (free plan default, no override)", ent.MaxDevices) } } // TestEntitlementForUser_FreeUserWithOverride: same free user, override=6 — // the free-fallback branch (no active subscription row) must be overridden // too, otherwise the connect backstop rejects the 2nd..6th device even // though the login gate (devices.ResolvePlan) already allows them. func TestEntitlementForUser_FreeUserWithOverride(t *testing.T) { db := openEntitlementTestDB(t) st := nodes.NewSQLNodeStore(db) userID := insertEntitlementTestUser(t, db, "free-override@example.com", sql.NullInt64{Int64: 6, Valid: true}) ent, err := st.EntitlementForUser(context.Background(), userID) if err != nil { t.Fatalf("EntitlementForUser: %v", err) } if ent.PlanCode != "free" { t.Errorf("PlanCode = %q, want free (override must not change plan identity)", ent.PlanCode) } if ent.MaxDevices != 6 { t.Errorf("MaxDevices = %d, want 6 (override) — connect backstop must agree with login gate", ent.MaxDevices) } } // TestEntitlementForUser_ProSubscriptionOverrideWins: a pro-subscription user // (plan default max_devices=3, migration 000019) with override=2 — the // subscription branch must also be overridden, even though the override is // *smaller* than the plan's own cap (operators can tighten a specific // account too, per internal/devices/store.go:305-306's documented semantics). func TestEntitlementForUser_ProSubscriptionOverrideWins(t *testing.T) { db := openEntitlementTestDB(t) st := nodes.NewSQLNodeStore(db) userID := insertEntitlementTestUser(t, db, "pro-override@example.com", sql.NullInt64{Int64: 2, Valid: true}) giveEntitlementTestSubscription(t, db, userID, "pro", "trial", time.Now().UTC().Add(24*time.Hour)) ent, err := st.EntitlementForUser(context.Background(), userID) if err != nil { t.Fatalf("EntitlementForUser: %v", err) } if ent.PlanCode != "pro" { t.Errorf("PlanCode = %q, want pro", ent.PlanCode) } if ent.MaxDevices != 2 { t.Errorf("MaxDevices = %d, want 2 (override wins over pro plan's own cap of 3)", ent.MaxDevices) } } // TestEntitlementForUser_NullOrZeroOverrideDoesNotApply: NULL and 0 both mean // "no override" — the plan-derived cap must stand (free=1 here). func TestEntitlementForUser_NullOrZeroOverrideDoesNotApply(t *testing.T) { db := openEntitlementTestDB(t) st := nodes.NewSQLNodeStore(db) nullUser := insertEntitlementTestUser(t, db, "null-override@example.com", sql.NullInt64{}) zeroUser := insertEntitlementTestUser(t, db, "zero-override@example.com", sql.NullInt64{Int64: 0, Valid: true}) for _, tc := range []struct { name string userID int64 }{ {"NULL", nullUser}, {"zero", zeroUser}, } { t.Run(tc.name, func(t *testing.T) { ent, err := st.EntitlementForUser(context.Background(), tc.userID) if err != nil { t.Fatalf("EntitlementForUser: %v", err) } if ent.MaxDevices != 1 { t.Errorf("MaxDevices = %d, want 1 (no override should apply)", ent.MaxDevices) } }) } }