carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "net/http"
6 "net/url"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/registry"
12)
13
14// A name whose platform apex a sibling already holds is not free. The
15// instance door refuses a sibling's apex once the sibling exists; this is the
16// other order — the instance first, while the name was free, and the app after
17// it — which would leave the new app with no URL of its own for ever.
18func TestANameWhoseApexASiblingHoldsIsNotFree(t *testing.T) {
19 fx, member := instanceFixture(t, "code-apex-claim")
20 fx.srv.cfg.AppsDomain = "oncarlos.test"
21 domain := fx.sqid + ".oncarlos.test"
22 if status, _, out := rawReq(t, http.MethodPut, enableURL(fx, fx.sqid, "titogo"), member, `{}`); status != http.StatusOK {
23 t.Fatalf("enable = %d %s", status, out)
24 }
25 // While no app is called acme, titogo may take acme's label.
26 if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, fx.sqid, "titogo"), member, `{"host":"acme.`+domain+`"}`); status != http.StatusCreated {
27 t.Fatalf("instance at a free label = %d %s", status, out)
28 }
29 newApp := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/new"
30
31 // An instance record is enough: the reconciler would make it a route.
32 status, out := postJSON(t, newApp, map[string]string{"name": "acme"}, member)
33 if status != http.StatusConflict {
34 t.Fatalf("creating acme over titogo's instance = %d %v, want 409", status, out)
35 }
36 if msg, _ := out["error"].(string); !strings.Contains(msg, "acme."+domain) || !strings.Contains(msg, "titogo") {
37 t.Errorf("the refusal should name the address and who holds it: %q", msg)
38 }
39 if has, _ := fx.st.HasAppIn(OpsAccountID, "acme"); has {
40 t.Error("acme was created")
41 }
42
43 // So is a route on its own, whatever put it there.
44 fx.addRoute(t, registry.Route{Host: "beta." + domain, App: "titogo", Account: fx.sqid, Kind: "instance"})
45 if status, out := postJSON(t, newApp, map[string]string{"name": "beta"}, member); status != http.StatusConflict {
46 t.Fatalf("creating beta over titogo's route = %d %v, want 409", status, out)
47 }
48
49 // A route that names no app holds the host just as firmly: the domains
50 // pass defers to it all the same.
51 fx.addRoute(t, registry.Route{Host: "delta." + domain, Account: fx.sqid, Kind: "proxy"})
52 status, out = postJSON(t, newApp, map[string]string{"name": "delta"}, member)
53 if status != http.StatusConflict {
54 t.Fatalf("creating delta over an app-less route = %d %v, want 409", status, out)
55 }
56 if msg, _ := out["error"].(string); !strings.Contains(msg, "no app") {
57 t.Errorf("the refusal should say the route has no app: %q", msg)
58 }
59
60 // Nor is a route that wears the name but serves another account the
61 // name's own.
62 fx.addRoute(t, registry.Route{Host: "epsilon." + domain, App: "epsilon", Account: "zzzz", Kind: "instance"})
63 status, out = postJSON(t, newApp, map[string]string{"name": "epsilon"}, member)
64 if status != http.StatusConflict {
65 t.Fatalf("creating epsilon over another account's route = %d %v, want 409", status, out)
66 }
67 if msg, _ := out["error"].(string); !strings.Contains(msg, "zzzz") {
68 t.Errorf("the refusal should name the account the route serves: %q", msg)
69 }
70
71 // Trashing the sibling does not free the name: its record survives, and
72 // restoring it would bring the instance back.
73 if err := fx.st.TrashAppRow(OpsAccountID, "titogo", time.Now()); err != nil {
74 t.Fatal(err)
75 }
76 status, out = postJSON(t, newApp, map[string]string{"name": "acme"}, member)
77 if status != http.StatusConflict {
78 t.Fatalf("creating acme over a trashed titogo's instance = %d %v, want 409", status, out)
79 }
80 if msg, _ := out["error"].(string); !strings.Contains(msg, "trash") {
81 t.Errorf("the refusal should say titogo is in the trash: %q", msg)
82 }
83
84 // A name nobody holds is still just a name.
85 if status, out := postJSON(t, newApp, map[string]string{"name": "gamma"}, member); status != http.StatusOK {
86 t.Fatalf("creating a free name = %d %v, want 200", status, out)
87 }
88}
89
90// Renaming onto a name whose apex a sibling holds is the same arrival by
91// another door, and is refused the same way.
92func TestRenameOntoAHeldApexIsRefused(t *testing.T) {
93 fx := newConsoleFixture(t)
94 fx.srv.cfg.AppsDomain = "oncarlos.test"
95 freshApp(t, fx, "sprout")
96 fx.addRoute(t, registry.Route{Host: "sapling." + fx.sqid + ".oncarlos.test", App: "kass", Account: fx.sqid, Kind: "instance"})
97 c := signInPaul(t, fx)
98 csrf := sessionToken(t, fx, c)
99 form := url.Values{"csrf": {csrf}, "to": {"sapling"}, "confirmed": {"1"}}
100 if status, loc, body := postForm(t, c, fx.u("/apps/sprout/rename"), form); strings.Contains(body, "Rename sprout to sapling?") {
101 t.Fatalf("offered the rename onto a held apex = %d -> %q", status, loc)
102 }
103 if has, _ := fx.st.HasAppIn(OpsAccountID, "sapling"); has {
104 t.Error("sprout was renamed onto an apex kass holds")
105 }
106 if has, _ := fx.st.HasAppIn(OpsAccountID, "sprout"); !has {
107 t.Error("sprout should be untouched")
108 }
109}
110
111// Moving an app into an account where another app holds its apex there is
112// the third arrival, and is refused before the card.
113func TestMoveIntoAnAccountWhereTheApexIsHeldIsRefused(t *testing.T) {
114 fx := newConsoleFixture(t)
115 fx.srv.cfg.AppsDomain = "oncarlos.test"
116 const destID = 4242
117 if err := fx.st.CreateAccount(destID, "cdef", "Second", "paul@keymail.dev", time.Now()); err != nil {
118 t.Fatal(err)
119 }
120 stampKassRoutes(t, fx, "cdef")
121 fx.addRoute(t, registry.Route{Host: "kass.cdef.oncarlos.test", App: "other", Account: "cdef", Kind: "instance"})
122 c := signInPaul(t, fx)
123 csrf := sessionToken(t, fx, c)
124 // Not confirmed: without the check this renders the move card, so the
125 // card's absence is the refusal.
126 status, _, body := postForm(t, c, fx.u("/apps/kass/move"), url.Values{"csrf": {csrf}})
127 if strings.Contains(body, "Move kass to Second?") {
128 t.Fatalf("offered the move into an account where the apex is held = %d", status)
129 }
130 if has, _ := fx.st.HasAppIn(destID, "kass"); has {
131 t.Error("kass was moved")
132 }
133 if has, _ := fx.st.HasAppIn(OpsAccountID, "kass"); !has {
134 t.Error("kass should still be in its own account")
135 }
136}
137
138// At boot the console names every route already sitting at another app's
139// apex — live or trashed, with another app, no app, or another account on
140// the route — and nothing else: an app's own apex, a label no app wears, and
141// hosts outside the apps domain are not squats.
142func TestTheStartupReportNamesSquatsAndNothingElse(t *testing.T) {
143 fx := newConsoleFixture(t)
144 fx.srv.cfg.AppsDomain = "oncarlos.test"
145 now := time.Now()
146 for _, app := range []string{"acme", "parked", "blank", "elsewhere"} {
147 if _, err := fx.st.CreateAppIn(OpsAccountID, app, "paul@keymail.dev", now); err != nil {
148 t.Fatal(err)
149 }
150 }
151 if err := fx.st.TrashAppRow(OpsAccountID, "parked", now); err != nil {
152 t.Fatal(err)
153 }
154 d := fx.sqid + ".oncarlos.test"
155 for _, rt := range []registry.Route{
156 {Host: "acme." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // squat on a live app
157 {Host: "parked." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // squat on a trashed one
158 {Host: "blank." + d, Account: fx.sqid, Kind: "proxy"}, // squat with no app
159 {Host: "elsewhere." + d, App: "elsewhere", Account: "zzzz", Kind: "proxy"}, // right name, wrong account
160 {Host: "titogo." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // its own apex
161 {Host: "nobody." + d, App: "titogo", Account: fx.sqid, Kind: "instance"}, // no app by that name
162 {Host: "acme.example.com", App: "titogo", Account: fx.sqid, Kind: "instance"},
163 } {
164 fx.addRoute(t, rt)
165 }
166 got := fx.srv.apexSquats(context.Background())
167 if len(got) != 4 {
168 t.Fatalf("report = %q, want the four squats", got)
169 }
170 joined := strings.Join(got, "\n")
171 for _, want := range []string{"acme." + d, "parked." + d, "in the trash", "belongs to titogo",
172 "blank." + d, "belongs to no app", "elsewhere." + d, "(zzzz)"} {
173 if !strings.Contains(joined, want) {
174 t.Errorf("report missing %q:\n%s", want, joined)
175 }
176 }
177}
178