feat(surf): print the serve URL and admin status on startup

- Add surf.Router.AdminPrefix (additive): the admin mount path and true
  when BuildRouter mounted the cabana admin, false otherwise
- serve binds with net.Listen before printing, so a busy port fails
  without a misleading URL, then prints the listen address with a
  browser URL and an admin status line: the admin URL, a warning naming
  summer admin:create when backend_users is empty or missing, or
  admin: off with the steps to add an admin controller
- The administrator count is bounded by a 2 s timeout and never fails boot
- route:list ends with the same admin status line
- surf and cabana READMEs and the console and routing docs describe the output
This commit is contained in:
Jakub Zych
2026-10-08 16:36:21 +02:00
parent 4b5ed7707e
commit da21670870
10 changed files with 294 additions and 10 deletions

View File

@@ -1,12 +1,15 @@
package surf
import (
"bytes"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/compass"
"gocloud.dev/blob"
)
@@ -51,3 +54,71 @@ func serveTestApp(t *testing.T, storageYAML string) *backpack.App {
}
return backpack.New(cfg)
}
func TestServeURL(t *testing.T) {
for _, tc := range []struct{ addr, want string }{
{":8123", "http://127.0.0.1:8123/"},
{"127.0.0.1:9000", "http://127.0.0.1:9000/"},
{"[::1]:8080", "http://[::1]:8080/"},
{"0.0.0.0:80", "http://127.0.0.1/"},
{"[::]:8080", "http://127.0.0.1:8080/"},
{"localhost:3000", "http://localhost:3000/"},
{"8080", ""},
{"", ""},
{"127.0.0.1:", ""},
} {
if got := serveURL(tc.addr); got != tc.want {
t.Errorf("serveURL(%q) = %q, want %q", tc.addr, got, tc.want)
}
}
}
func TestServeBanner(t *testing.T) {
listen := bannerLine{text: "listening on :8123, open http://127.0.0.1:8123/"}
mounted := bannerLine{text: "admin: http://127.0.0.1:8123/backend"}
create := "summer admin:create --email you@example.com --superuser"
for _, tc := range []struct {
name string
addr string
prefix string
mounted bool
admins adminPresence
want []bannerLine
}{
{"present", ":8123", "/backend", true, adminsPresent, []bannerLine{listen, mounted}},
{"none", ":8123", "/backend", true, adminsNone, []bannerLine{listen, mounted,
{warn: true, text: "no administrators yet; create one with " + create}}},
{"missing table", ":8123", "/backend", true, adminsMissingTable, []bannerLine{listen, mounted,
{warn: true, text: "the admin tables do not exist yet; run summer migrate, then " + create}}},
{"unknown", ":8123", "/backend", true, adminsUnknown, []bannerLine{listen, mounted,
{text: "sign in with an administrator; " + create + " creates one"}}},
{"off", ":8123", "", false, adminsUnknown, []bannerLine{listen,
{text: "admin: off — no plugin registers an admin controller"},
{text: "to add one, run summer make:admin-controller <vendor.plugin> <Name>, then summer build, summer migrate and " + create + "; the admin needs SUMMER_ADMIN__JWT__SECRET, which summer new writes to .env"}}},
{"unparsable address", "8080", "/backend", true, adminsPresent, []bannerLine{
{text: "listening on 8080"}, {text: "admin: mounted at /backend"}}},
} {
t.Run(tc.name, func(t *testing.T) {
got := serveBanner(tc.addr, tc.prefix, tc.mounted, tc.admins)
if !slices.Equal(got, tc.want) {
t.Fatalf("serveBanner:\n got %#v\nwant %#v", got, tc.want)
}
})
}
}
func TestPrintBanner(t *testing.T) {
var buf bytes.Buffer
out := bonfire.NewOutput(strings.NewReader(""), &buf, &buf)
printBanner(out, []bannerLine{{text: "plain"}, {warn: true, text: "careful"}})
got := buf.String()
if !strings.Contains(got, "ℹ plain") || !strings.Contains(got, "⚠ careful") {
t.Fatalf("printBanner output = %q", got)
}
}
func TestCountAdminsNilDB(t *testing.T) {
if got := countAdmins(t.Context(), nil); got != adminsUnknown {
t.Fatalf("countAdmins(nil) = %v, want adminsUnknown", got)
}
}