package api import ( "context" "encoding/json" "errors" "net/http" "testing" ) // fakeBroker records what the server asked the broker to do. type fakeBroker struct { ensured map[string]string deleted []string fail error } func (b *fakeBroker) EnsureSite(_ context.Context, user, pass string) error { if b.fail != nil { return b.fail } if b.ensured == nil { b.ensured = map[string]string{} } b.ensured[user] = pass return nil } func (b *fakeBroker) DeleteSite(_ context.Context, user string) error { b.deleted = append(b.deleted, user) return nil } func ownerSession(t *testing.T, s *Server, fs *fakeStore) Session { t.Helper() fs.addUser("owner@acme.com", "correct horse battery", UserRecord{ ID: "u-owner", ClientID: "client-acme", Role: "owner", Active: true, }) return login(t, s, "owner@acme.com", "correct horse battery") } func TestAnOwnerOpensAShopAndTheBrokerLearnsOfIt(t *testing.T) { s, fs := newServer(t) b := &fakeBroker{} s.Broker = b sess := ownerSession(t, s, fs) rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Acme Bengaluru!"}) if rec.Code != http.StatusCreated { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } var out map[string]any _ = json.Unmarshal(rec.Body.Bytes(), &out) if out["slug"] != "acme-bengaluru" { t.Errorf("slug not derived from the name: %v", out["slug"]) } if _, leaked := out["password"]; leaked { t.Fatal("the broker password was serialised") } if b.ensured["acme.acme-bengaluru"] == "" { t.Fatalf("broker was not told about the shop: %+v", b.ensured) } if len(fs.sites) != 1 { t.Fatalf("expected one site, have %d", len(fs.sites)) } } func TestABrokerFailureLeavesNoHalfMadeShop(t *testing.T) { s, fs := newServer(t) s.Broker = &fakeBroker{fail: errors.New("no answer on the control topic")} sess := ownerSession(t, s, fs) rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Ghost"}) if rec.Code != http.StatusBadGateway { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } if len(fs.sites) != 0 { t.Fatalf("a shop the broker never accepted was kept: %+v", fs.sites) } } func TestAManagerCannotOpenAShop(t *testing.T) { s, fs := newServer(t) s.Broker = &fakeBroker{} seedUser(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Nope"}) if rec.Code != http.StatusForbidden { t.Fatalf("manager opened a shop: %d", rec.Code) } } func TestADuplicateShortNameIsAConflict(t *testing.T) { s, fs := newServer(t) s.Broker = &fakeBroker{} seedSite(fs) sess := ownerSession(t, s, fs) rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Again", "slug": "chennai"}) if rec.Code != http.StatusConflict { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } } func TestNoBrokerConfiguredSaysSo(t *testing.T) { s, fs := newServer(t) sess := ownerSession(t, s, fs) rec := do(t, s, "POST", "/api/sites", sess.Token, map[string]any{"name": "Shop"}) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } } func TestAManagerRenamesAShopButTheSlugStays(t *testing.T) { s, fs := newServer(t) seedUser(fs) seedSite(fs) sess := login(t, s, "manager@acme.com", "correct horse battery") rec := do(t, s, "PATCH", "/api/sites/chennai", sess.Token, map[string]any{"name": "TeNext Coimbatore"}) if rec.Code != http.StatusOK { t.Fatalf("got %d: %s", rec.Code, rec.Body.String()) } var out SiteHealth _ = json.Unmarshal(rec.Body.Bytes(), &out) if out.Name != "TeNext Coimbatore" || out.Slug != "chennai" { t.Fatalf("renamed wrong: %+v", out) } if rec := do(t, s, "PATCH", "/api/sites/chennai", sess.Token, map[string]any{"timezone": "Mars/Olympus"}); rec.Code != http.StatusBadRequest { t.Fatalf("bad timezone accepted: %d", rec.Code) } }