package console import ( "errors" "path/filepath" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/plans" ) func planStore(t *testing.T, accounts ...string) (*Store, map[string]int64) { t.Helper() st, err := Open(filepath.Join(t.TempDir(), "console.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) ids := map[string]int64{} for i, sqid := range accounts { id := int64(100 + i) if err := st.CreateAccount(id, sqid, sqid, sqid+"@example.com", time.Unix(1_700_000_000, 0)); err != nil { t.Fatal(err) } ids[sqid] = id } return st, ids } var planNow = time.Unix(1_800_000_000, 0) func TestAccountPlanAbsentIsNil(t *testing.T) { st, ids := planStore(t, "aaa") row, err := st.AccountPlan(ids["aaa"]) if err != nil || row != nil { t.Fatalf("got %+v, %v; want nil, nil", row, err) } } func TestSetAccountPlanBumpsRevision(t *testing.T) { st, ids := planStore(t, "aaa") id := ids["aaa"] sp := &plans.Limits{RunningThings: 12} rev, err := st.SetAccountPlan(id, "personal", map[string]int{"apps": 2}, "sponsored", sp, "existing user", "ops@example.com", planNow) if err != nil || rev != 1 { t.Fatalf("first set: rev %d, %v", rev, err) } rev, err = st.SetAccountPlan(id, "personal", map[string]int{"apps": 3}, "sponsored", sp, "more apps", "ops@example.com", planNow) if err != nil || rev != 2 { t.Fatalf("second set: rev %d, %v", rev, err) } row, err := st.AccountPlan(id) if err != nil || row == nil { t.Fatalf("read back: %+v, %v", row, err) } if row.Sqid != "aaa" || row.Plan != "personal" || row.Source != "sponsored" || row.Reason != "more apps" || row.UpdatedBy != "ops@example.com" || row.Revision != 2 || !row.UpdatedAt.Equal(planNow) { t.Fatalf("row %+v", row) } if !reflect.DeepEqual(row.Packs, map[string]int{"apps": 3}) { t.Fatalf("packs %v", row.Packs) } if row.Sponsored == nil || *row.Sponsored != *sp { t.Fatalf("sponsored %+v", row.Sponsored) } if _, err := st.SetAccountPlan(id, "free", nil, "operator", nil, "back to free", "ops@example.com", planNow); err != nil { t.Fatal(err) } row, _ = st.AccountPlan(id) if row.Sponsored != nil || len(row.Packs) != 0 { t.Fatalf("cleared row kept sponsored %+v / packs %v", row.Sponsored, row.Packs) } } func TestEnsureDefaultPlansInsertsOnlyMissing(t *testing.T) { st, ids := planStore(t, "aaa", "bbb", "ccc") if _, err := st.SetAccountPlan(ids["bbb"], "personal", nil, "operator", nil, "paid", "ops@example.com", planNow); err != nil { t.Fatal(err) } n, err := st.EnsureDefaultPlans("free", planNow) if err != nil || n != 2 { t.Fatalf("inserted %d, %v; want 2", n, err) } b, _ := st.AccountPlan(ids["bbb"]) if b.Plan != "personal" || b.Revision != 1 { t.Fatalf("existing row changed: %+v", b) } a, _ := st.AccountPlan(ids["aaa"]) if a == nil || a.Plan != "free" || a.Source != "default" || a.Revision != 1 || a.UpdatedBy != "plans" { t.Fatalf("default row %+v", a) } if n, err := st.EnsureDefaultPlans("free", planNow); err != nil || n != 0 { t.Fatalf("second ensure inserted %d, %v", n, err) } } func TestEnsureDefaultPlansSkipsDeletedAccounts(t *testing.T) { st, ids := planStore(t, "aaa", "gone") if _, err := st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), ids["gone"]); err != nil { t.Fatal(err) } if n, err := st.EnsureDefaultPlans("free", planNow); err != nil || n != 1 { t.Fatalf("inserted %d, %v; want 1", n, err) } if row, _ := st.AccountPlan(ids["gone"]); row != nil { t.Fatalf("deleted account got a row: %+v", row) } } func TestPlansToPublish(t *testing.T) { st, ids := planStore(t, "aaa", "bbb", "ccc") if _, err := st.EnsureDefaultPlans("free", planNow); err != nil { t.Fatal(err) } rows, err := st.PlansToPublish("v1@abc", 0, 10) if err != nil || len(rows) != 3 { t.Fatalf("got %d rows, %v; want 3", len(rows), err) } if rows, _ := st.PlansToPublish("v1@abc", 0, 2); len(rows) != 2 { t.Fatalf("limit ignored: %d rows", len(rows)) } if err := st.MarkPlanPublished(ids["aaa"], 1, "v1@abc"); err != nil { t.Fatal(err) } rows, _ = st.PlansToPublish("v1@abc", 0, 10) for _, r := range rows { if r.Sqid == "aaa" { t.Fatal("published row still due for the same catalog") } } if len(rows) != 2 { t.Fatalf("got %d rows, want 2", len(rows)) } rows, _ = st.PlansToPublish("v2@def", 0, 10) if len(rows) != 3 { t.Fatalf("catalog change: got %d rows, want 3", len(rows)) } if _, err := st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), ids["bbb"]); err != nil { t.Fatal(err) } rows, _ = st.PlansToPublish("v2@def", 0, 10) if len(rows) != 2 { t.Fatalf("deleted account still due: got %d rows, want 2", len(rows)) } } func TestMarkPlanPublishedIgnoresAStaleRevision(t *testing.T) { st, ids := planStore(t, "aaa") id := ids["aaa"] for range 2 { if _, err := st.SetAccountPlan(id, "free", nil, "operator", nil, "r", "ops@example.com", planNow); err != nil { t.Fatal(err) } } if err := st.MarkPlanPublished(id, 1, "c"); err != nil { t.Fatal(err) } if row, _ := st.AccountPlan(id); row.PublishedRevision != 0 { t.Fatalf("stale publish marked: %d", row.PublishedRevision) } if err := st.MarkPlanPublished(id, 2, "c"); err != nil { t.Fatal(err) } if row, _ := st.AccountPlan(id); row.PublishedRevision != 2 || row.PublishedCatalog != "c" { t.Fatalf("published %d / %q", row.PublishedRevision, row.PublishedCatalog) } } // Astra plans-1 finding 3: a plan write must not land on a deleted account. func TestSetAccountPlanRefusesADeletedAccount(t *testing.T) { st, ids := planStore(t, "gone") if _, err := st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), ids["gone"]); err != nil { t.Fatal(err) } if _, err := st.SetAccountPlan(ids["gone"], "free", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); !errors.Is(err, ErrAccountGone) { t.Fatalf("err %v, want ErrAccountGone", err) } if row, _ := st.AccountPlan(ids["gone"]); row != nil { t.Fatalf("wrote %+v", row) } if _, err := st.SetAccountPlan(999999, "free", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); !errors.Is(err, ErrAccountGone) { t.Fatalf("unknown account: err %v, want ErrAccountGone", err) } }