| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/plans" |
| 9 | "github.com/carlosframework/platform/internal/provision" |
| 10 | ) |
| 11 | |
| 12 | func capsApp(t *testing.T, fx *consoleFixture, acct *Account, app string) { |
| 13 | t.Helper() |
| 14 | if _, err := fx.st.CreateAppIn(acct.ID, app, "owner@example.com", planNow); err != nil { |
| 15 | t.Fatal(err) |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | func capsInstance(t *testing.T, fx *consoleFixture, acct *Account, app, host string) { |
| 20 | t.Helper() |
| 21 | if _, err := fx.rel.Put(context.Background(), provision.RecordKey(acct.Sqid, app, host), []byte(`{}`), ""); err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | func capsSidecar(t *testing.T, fx *consoleFixture, acct *Account, app, name string) { |
| 27 | t.Helper() |
| 28 | if _, err := fx.rel.Put(context.Background(), provision.SidecarRecordKey(acct.Sqid, app, name), []byte(`{}`), ""); err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func isCap(err error) bool { |
| 34 | var c *planCapError |
| 35 | return errors.As(err, &c) |
| 36 | } |
| 37 | |
| 38 | func TestPlanChecksPassWithoutACatalog(t *testing.T) { |
| 39 | fx := newConsoleFixture(t) |
| 40 | acct := planAccount(t, fx, 501) |
| 41 | for i := range 5 { |
| 42 | capsApp(t, fx, acct, "app"+string(rune('a'+i))) |
| 43 | } |
| 44 | capsSidecar(t, fx, acct, "appa", "w1") |
| 45 | capsSidecar(t, fx, acct, "appa", "w2") |
| 46 | ctx := context.Background() |
| 47 | for name, err := range map[string]error{ |
| 48 | "running things": fx.srv.checkRunningThings(ctx, acct, 1), |
| 49 | "sidecars": fx.srv.checkSidecars(ctx, acct, "appa"), |
| 50 | "free account": fx.srv.checkNewFreeAccount("owner" + acct.Sqid + "@example.com"), |
| 51 | "fleets": fx.srv.checkFleets(acct), |
| 52 | "custom domains": fx.srv.checkCustomDomains(acct), |
| 53 | } { |
| 54 | if err != nil { |
| 55 | t.Errorf("%s with no catalog: %v", name, err) |
| 56 | } |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | func TestLiveAppCountCountsRestoringNotTrashed(t *testing.T) { |
| 61 | fx := newConsoleFixture(t) |
| 62 | acct := planAccount(t, fx, 501) |
| 63 | for _, app := range []string{"live", "gone", "coming-back"} { |
| 64 | capsApp(t, fx, acct, app) |
| 65 | } |
| 66 | if _, err := fx.st.db.Exec(`UPDATE apps SET deleted_at = ? WHERE account_id = ? AND app IN ('gone', 'coming-back')`, planNow.Unix(), acct.ID); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | if _, err := fx.st.db.Exec(`UPDATE apps SET restoring = 1 WHERE account_id = ? AND app = 'coming-back'`, acct.ID); err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | if n, err := fx.st.LiveAppCount(acct.ID); err != nil || n != 2 { |
| 73 | t.Fatalf("LiveAppCount = %d, %v; want 2 (live + restoring)", n, err) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | func TestRunningThingsCountsAppsAndInstances(t *testing.T) { |
| 78 | fx := newConsoleFixture(t) |
| 79 | acct := planAccount(t, fx, 501) |
| 80 | capsApp(t, fx, acct, "shop") |
| 81 | capsApp(t, fx, acct, "blog") |
| 82 | capsApp(t, fx, acct, "old") |
| 83 | capsInstance(t, fx, acct, "shop", "a.shop.example.com") |
| 84 | capsInstance(t, fx, acct, "shop", "b.shop.example.com") |
| 85 | capsInstance(t, fx, acct, "blog", "c.blog.example.com") |
| 86 | capsInstance(t, fx, acct, "old", "d.old.example.com") |
| 87 | if err := fx.st.TrashAppRow(acct.ID, "old", planNow); err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | if n, err := fx.srv.runningThings(context.Background(), acct); err != nil || n != 5 { |
| 91 | t.Fatalf("runningThings = %d, %v; want 5 (2 apps + 3 instances; the trashed app and its instance do not count)", n, err) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func TestRunningThingsCapAtAndBelow(t *testing.T) { |
| 96 | fx := newConsoleFixture(t) |
| 97 | fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) |
| 98 | acct := planAccount(t, fx, 501) |
| 99 | capsApp(t, fx, acct, "one") |
| 100 | capsApp(t, fx, acct, "two") |
| 101 | if err := fx.srv.checkRunningThings(context.Background(), acct, 1); err != nil { |
| 102 | t.Fatalf("at 2 of 3: %v", err) |
| 103 | } |
| 104 | capsApp(t, fx, acct, "three") |
| 105 | if err := fx.srv.checkRunningThings(context.Background(), acct, 1); !isCap(err) { |
| 106 | t.Fatalf("at 3 of 3: %v, want a plan cap", err) |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | func TestRunningThingsCapUsesEffectiveLimits(t *testing.T) { |
| 111 | fx := newConsoleFixture(t) |
| 112 | fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) |
| 113 | acct := planAccount(t, fx, 501) |
| 114 | if _, err := fx.st.SetAccountPlan(acct.ID, "free", nil, PlanSourceSponsored, &plans.Limits{RunningThings: 300}, "existing account", "ops@example.com", planNow); err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | for i := range 4 { |
| 118 | capsApp(t, fx, acct, "app"+string(rune('a'+i))) |
| 119 | } |
| 120 | if err := fx.srv.checkRunningThings(context.Background(), acct, 1); err != nil { |
| 121 | t.Fatalf("sponsored 300, running 4: %v", err) |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestSidecarCap(t *testing.T) { |
| 126 | fx := newConsoleFixture(t) |
| 127 | fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) |
| 128 | acct := planAccount(t, fx, 501) |
| 129 | capsApp(t, fx, acct, "shop") |
| 130 | if err := fx.srv.checkSidecars(context.Background(), acct, "shop"); err != nil { |
| 131 | t.Fatalf("free, no sidecars: %v", err) |
| 132 | } |
| 133 | capsSidecar(t, fx, acct, "shop", "w1") |
| 134 | if err := fx.srv.checkSidecars(context.Background(), acct, "shop"); !isCap(err) { |
| 135 | t.Fatalf("free, one sidecar: %v, want a plan cap", err) |
| 136 | } |
| 137 | if _, err := fx.st.SetAccountPlan(acct.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil { |
| 138 | t.Fatal(err) |
| 139 | } |
| 140 | for _, n := range []string{"w2", "w3", "w4"} { |
| 141 | capsSidecar(t, fx, acct, "shop", n) |
| 142 | } |
| 143 | if err := fx.srv.checkSidecars(context.Background(), acct, "shop"); err != nil { |
| 144 | t.Fatalf("personal, four sidecars: %v", err) |
| 145 | } |
| 146 | capsSidecar(t, fx, acct, "shop", "w5") |
| 147 | if err := fx.srv.checkSidecars(context.Background(), acct, "shop"); !isCap(err) { |
| 148 | t.Fatalf("personal, five sidecars: %v, want a plan cap", err) |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | func TestNewFreeAccountCap(t *testing.T) { |
| 153 | fx := newConsoleFixture(t) |
| 154 | fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) |
| 155 | acct := planAccount(t, fx, 501) |
| 156 | owner := "owner" + acct.Sqid + "@example.com" |
| 157 | if err := fx.srv.checkNewFreeAccount(owner); !isCap(err) { |
| 158 | t.Fatalf("owner of one free account: %v, want a plan cap", err) |
| 159 | } |
| 160 | if _, err := fx.st.SetAccountPlan(acct.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil { |
| 161 | t.Fatal(err) |
| 162 | } |
| 163 | if err := fx.srv.checkNewFreeAccount(owner); err != nil { |
| 164 | t.Fatalf("owner of one paid account: %v", err) |
| 165 | } |
| 166 | member := "member@example.com" |
| 167 | other := planAccount(t, fx, 502) |
| 168 | if err := fx.st.AddMember(other.ID, member, RoleMember, planNow); err != nil { |
| 169 | t.Fatal(err) |
| 170 | } |
| 171 | if err := fx.srv.checkNewFreeAccount(member); err != nil { |
| 172 | t.Fatalf("member, not owner, of a free account: %v", err) |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | func TestFleetsAndCustomDomainsFollowThePlan(t *testing.T) { |
| 177 | fx := newConsoleFixture(t) |
| 178 | fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) |
| 179 | acct := planAccount(t, fx, 501) |
| 180 | if !isCap(fx.srv.checkFleets(acct)) || !isCap(fx.srv.checkCustomDomains(acct)) { |
| 181 | t.Fatal("free plan allowed fleets or custom domains") |
| 182 | } |
| 183 | if _, err := fx.st.SetAccountPlan(acct.ID, "personal", nil, PlanSourceOperator, nil, "r", "ops@example.com", planNow); err != nil { |
| 184 | t.Fatal(err) |
| 185 | } |
| 186 | if fx.srv.checkFleets(acct) != nil || fx.srv.checkCustomDomains(acct) != nil { |
| 187 | t.Fatal("personal plan refused fleets or custom domains") |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | // Astra plans-3a finding 1: restoring an app brings back its retained |
| 192 | // instance records too, so the check must count the whole footprint. |
| 193 | func TestRestoreCountsTheAppsInstances(t *testing.T) { |
| 194 | fx := newConsoleFixture(t) |
| 195 | fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) |
| 196 | acct := planAccount(t, fx, 501) |
| 197 | capsApp(t, fx, acct, "live") |
| 198 | capsApp(t, fx, acct, "old") |
| 199 | capsInstance(t, fx, acct, "old", "a.old.example.com") |
| 200 | capsInstance(t, fx, acct, "old", "b.old.example.com") |
| 201 | if err := fx.st.TrashAppRow(acct.ID, "old", planNow); err != nil { |
| 202 | t.Fatal(err) |
| 203 | } |
| 204 | // One running thing of three: restoring "old" would make four. |
| 205 | if err := fx.srv.checkRestore(context.Background(), acct, "old"); !isCap(err) { |
| 206 | t.Fatalf("restore bringing back an app and two instances at 1 of 3: %v, want a plan cap", err) |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | // Astra plans-3a finding 3: adopting an app whose objects already exist in |
| 211 | // the bucket brings its instance records with it. |
| 212 | func TestCreateCountsAnAdoptedAppsInstances(t *testing.T) { |
| 213 | fx := newConsoleFixture(t) |
| 214 | fx.srv.cfg.PlanCatalog = examplePlanCatalog(t) |
| 215 | acct := planAccount(t, fx, 501) |
| 216 | capsApp(t, fx, acct, "live") |
| 217 | capsInstance(t, fx, acct, "legacy", "a.legacy.example.com") |
| 218 | capsInstance(t, fx, acct, "legacy", "b.legacy.example.com") |
| 219 | if err := fx.srv.checkCreateApp(context.Background(), acct, "legacy"); !isCap(err) { |
| 220 | t.Fatalf("adopting an app with two instances at 1 of 3: %v, want a plan cap", err) |
| 221 | } |
| 222 | if err := fx.srv.checkCreateApp(context.Background(), acct, "fresh"); err != nil { |
| 223 | t.Fatalf("a new app at 1 of 3: %v", err) |
| 224 | } |
| 225 | } |
| 226 | |