| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "path/filepath" |
| 6 | "reflect" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/plans" |
| 11 | ) |
| 12 | |
| 13 | func planStore(t *testing.T, accounts ...string) (*Store, map[string]int64) { |
| 14 | t.Helper() |
| 15 | st, err := Open(filepath.Join(t.TempDir(), "console.db")) |
| 16 | if err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | t.Cleanup(func() { st.Close() }) |
| 20 | ids := map[string]int64{} |
| 21 | for i, sqid := range accounts { |
| 22 | id := int64(100 + i) |
| 23 | if err := st.CreateAccount(id, sqid, sqid, sqid+"@example.com", time.Unix(1_700_000_000, 0)); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | ids[sqid] = id |
| 27 | } |
| 28 | return st, ids |
| 29 | } |
| 30 | |
| 31 | var planNow = time.Unix(1_800_000_000, 0) |
| 32 | |
| 33 | func TestAccountPlanAbsentIsNil(t *testing.T) { |
| 34 | st, ids := planStore(t, "aaa") |
| 35 | row, err := st.AccountPlan(ids["aaa"]) |
| 36 | if err != nil || row != nil { |
| 37 | t.Fatalf("got %+v, %v; want nil, nil", row, err) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func TestSetAccountPlanBumpsRevision(t *testing.T) { |
| 42 | st, ids := planStore(t, "aaa") |
| 43 | id := ids["aaa"] |
| 44 | sp := &plans.Limits{RunningThings: 12} |
| 45 | rev, err := st.SetAccountPlan(id, "personal", map[string]int{"apps": 2}, "sponsored", sp, "existing user", "ops@example.com", planNow) |
| 46 | if err != nil || rev != 1 { |
| 47 | t.Fatalf("first set: rev %d, %v", rev, err) |
| 48 | } |
| 49 | rev, err = st.SetAccountPlan(id, "personal", map[string]int{"apps": 3}, "sponsored", sp, "more apps", "ops@example.com", planNow) |
| 50 | if err != nil || rev != 2 { |
| 51 | t.Fatalf("second set: rev %d, %v", rev, err) |
| 52 | } |
| 53 | row, err := st.AccountPlan(id) |
| 54 | if err != nil || row == nil { |
| 55 | t.Fatalf("read back: %+v, %v", row, err) |
| 56 | } |
| 57 | if row.Sqid != "aaa" || row.Plan != "personal" || row.Source != "sponsored" || row.Reason != "more apps" || |
| 58 | row.UpdatedBy != "ops@example.com" || row.Revision != 2 || !row.UpdatedAt.Equal(planNow) { |
| 59 | t.Fatalf("row %+v", row) |
| 60 | } |
| 61 | if !reflect.DeepEqual(row.Packs, map[string]int{"apps": 3}) { |
| 62 | t.Fatalf("packs %v", row.Packs) |
| 63 | } |
| 64 | if row.Sponsored == nil || *row.Sponsored != *sp { |
| 65 | t.Fatalf("sponsored %+v", row.Sponsored) |
| 66 | } |
| 67 | if _, err := st.SetAccountPlan(id, "free", nil, "operator", nil, "back to free", "ops@example.com", planNow); err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | row, _ = st.AccountPlan(id) |
| 71 | if row.Sponsored != nil || len(row.Packs) != 0 { |
| 72 | t.Fatalf("cleared row kept sponsored %+v / packs %v", row.Sponsored, row.Packs) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | func TestEnsureDefaultPlansInsertsOnlyMissing(t *testing.T) { |
| 77 | st, ids := planStore(t, "aaa", "bbb", "ccc") |
| 78 | if _, err := st.SetAccountPlan(ids["bbb"], "personal", nil, "operator", nil, "paid", "ops@example.com", planNow); err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | n, err := st.EnsureDefaultPlans("free", planNow) |
| 82 | if err != nil || n != 2 { |
| 83 | t.Fatalf("inserted %d, %v; want 2", n, err) |
| 84 | } |
| 85 | b, _ := st.AccountPlan(ids["bbb"]) |
| 86 | if b.Plan != "personal" || b.Revision != 1 { |
| 87 | t.Fatalf("existing row changed: %+v", b) |
| 88 | } |
| 89 | a, _ := st.AccountPlan(ids["aaa"]) |
| 90 | if a == nil || a.Plan != "free" || a.Source != "default" || a.Revision != 1 || a.UpdatedBy != "plans" { |
| 91 | t.Fatalf("default row %+v", a) |
| 92 | } |
| 93 | if n, err := st.EnsureDefaultPlans("free", planNow); err != nil || n != 0 { |
| 94 | t.Fatalf("second ensure inserted %d, %v", n, err) |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func TestEnsureDefaultPlansSkipsDeletedAccounts(t *testing.T) { |
| 99 | st, ids := planStore(t, "aaa", "gone") |
| 100 | if _, err := st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), ids["gone"]); err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | if n, err := st.EnsureDefaultPlans("free", planNow); err != nil || n != 1 { |
| 104 | t.Fatalf("inserted %d, %v; want 1", n, err) |
| 105 | } |
| 106 | if row, _ := st.AccountPlan(ids["gone"]); row != nil { |
| 107 | t.Fatalf("deleted account got a row: %+v", row) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | func TestPlansToPublish(t *testing.T) { |
| 112 | st, ids := planStore(t, "aaa", "bbb", "ccc") |
| 113 | if _, err := st.EnsureDefaultPlans("free", planNow); err != nil { |
| 114 | t.Fatal(err) |
| 115 | } |
| 116 | rows, err := st.PlansToPublish("v1@abc", 0, 10) |
| 117 | if err != nil || len(rows) != 3 { |
| 118 | t.Fatalf("got %d rows, %v; want 3", len(rows), err) |
| 119 | } |
| 120 | if rows, _ := st.PlansToPublish("v1@abc", 0, 2); len(rows) != 2 { |
| 121 | t.Fatalf("limit ignored: %d rows", len(rows)) |
| 122 | } |
| 123 | if err := st.MarkPlanPublished(ids["aaa"], 1, "v1@abc"); err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | rows, _ = st.PlansToPublish("v1@abc", 0, 10) |
| 127 | for _, r := range rows { |
| 128 | if r.Sqid == "aaa" { |
| 129 | t.Fatal("published row still due for the same catalog") |
| 130 | } |
| 131 | } |
| 132 | if len(rows) != 2 { |
| 133 | t.Fatalf("got %d rows, want 2", len(rows)) |
| 134 | } |
| 135 | rows, _ = st.PlansToPublish("v2@def", 0, 10) |
| 136 | if len(rows) != 3 { |
| 137 | t.Fatalf("catalog change: got %d rows, want 3", len(rows)) |
| 138 | } |
| 139 | if _, err := st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), ids["bbb"]); err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | rows, _ = st.PlansToPublish("v2@def", 0, 10) |
| 143 | if len(rows) != 2 { |
| 144 | t.Fatalf("deleted account still due: got %d rows, want 2", len(rows)) |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | func TestMarkPlanPublishedIgnoresAStaleRevision(t *testing.T) { |
| 149 | st, ids := planStore(t, "aaa") |
| 150 | id := ids["aaa"] |
| 151 | for range 2 { |
| 152 | if _, err := st.SetAccountPlan(id, "free", nil, "operator", nil, "r", "ops@example.com", planNow); err != nil { |
| 153 | t.Fatal(err) |
| 154 | } |
| 155 | } |
| 156 | if err := st.MarkPlanPublished(id, 1, "c"); err != nil { |
| 157 | t.Fatal(err) |
| 158 | } |
| 159 | if row, _ := st.AccountPlan(id); row.PublishedRevision != 0 { |
| 160 | t.Fatalf("stale publish marked: %d", row.PublishedRevision) |
| 161 | } |
| 162 | if err := st.MarkPlanPublished(id, 2, "c"); err != nil { |
| 163 | t.Fatal(err) |
| 164 | } |
| 165 | if row, _ := st.AccountPlan(id); row.PublishedRevision != 2 || row.PublishedCatalog != "c" { |
| 166 | t.Fatalf("published %d / %q", row.PublishedRevision, row.PublishedCatalog) |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | // Astra plans-1 finding 3: a plan write must not land on a deleted account. |
| 171 | func TestSetAccountPlanRefusesADeletedAccount(t *testing.T) { |
| 172 | st, ids := planStore(t, "gone") |
| 173 | if _, err := st.db.Exec(`UPDATE accounts SET deleted_at = ? WHERE id = ?`, planNow.Unix(), ids["gone"]); err != nil { |
| 174 | t.Fatal(err) |
| 175 | } |
| 176 | if _, err := st.SetAccountPlan(ids["gone"], "free", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); !errors.Is(err, ErrAccountGone) { |
| 177 | t.Fatalf("err %v, want ErrAccountGone", err) |
| 178 | } |
| 179 | if row, _ := st.AccountPlan(ids["gone"]); row != nil { |
| 180 | t.Fatalf("wrote %+v", row) |
| 181 | } |
| 182 | if _, err := st.SetAccountPlan(999999, "free", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); !errors.Is(err, ErrAccountGone) { |
| 183 | t.Fatalf("unknown account: err %v, want ErrAccountGone", err) |
| 184 | } |
| 185 | } |
| 186 | |