Files
Behavision/server/internal/api/sites_test.go
Suriyakumarvijayanayagam f88d441bbf A shop can be renamed and, while empty, removed - from head office
The display name was always meant to be editable and the slug frozen;
until now neither had a way in. PATCH /api/sites/{site} takes a name
and a timezone (manager and above), DELETE removes an empty shop
(owner). The shop drawer in head office gets both, with the short name
shown read-only and the reason beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-19 16:11:07 +05:30

130 lines
3.8 KiB
Go

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)
}
}