Test code-testable endpoints

This commit is contained in:
Jonathon Leight
2026-07-02 20:34:22 -04:00
parent 4bfcfd18f4
commit 82c1356abc
5 changed files with 621 additions and 0 deletions
+28
View File
@@ -2,6 +2,8 @@ package core
import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
@@ -12,6 +14,32 @@ import (
"github.com/jleight/meshtender/internal/testdb"
)
// appLogin creates a user and returns it plus a live app-host session cookie,
// established through the real /session/callback handoff. Endpoint tests pass the
// returned cookie to post()/do() to drive authenticated app routes.
func appLogin(t *testing.T, ts *httptest.Server, st *store.Store, ctx context.Context, host, username string) (*store.User, *http.Cookie) {
t.Helper()
u, err := st.CreateUser(ctx, username, "")
if err != nil {
t.Fatalf("create user: %v", err)
}
loginID, err := st.CreateLogin(ctx, u.ID)
if err != nil {
t.Fatalf("create login: %v", err)
}
code, err := st.CreateAuthCode(ctx, u.ID, loginID, "/")
if err != nil {
t.Fatalf("create auth code: %v", err)
}
resp := do(t, ts, host, "/session/callback?code="+code+"&state=s1", &http.Cookie{Name: "mt_state", Value: "s1"})
resp.Body.Close()
c := cookieByName(resp, "meshtender_session")
if c == nil {
t.Fatalf("no app session cookie after handoff for %q", username)
}
return u, c
}
// testConfig/testAuthConfig give the integration tests the app/auth/root hosts,
// using the same constants as splitServer (testAuthHost/testAppHost/testRootHost/
// testWWWHost, defined in handoff_test.go). Requests to a plain httptest listener
+131
View File
@@ -0,0 +1,131 @@
package core
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
// Black-box coverage for the auth host's non-visual POST endpoints: password
// sign-in/sign-up and the account-management forms. WebAuthn API ceremonies
// (#2025) need a virtual authenticator and are left to browser/manual checks.
// authSSO signs up a new user via the password form and returns its live SSO
// session cookie — the automatable way to mint an authenticated auth-host session.
func authSSO(t *testing.T, ts *httptest.Server, h hostEnv, username string) *http.Cookie {
t.Helper()
resp := post(t, ts, h.auth, "/signup/password", url.Values{"username": {username}, "password": {"supersecret"}})
resp.Body.Close()
c := cookieByName(resp, "meshtender_session")
if c == nil {
t.Fatalf("no SSO session cookie after signup for %q", username)
}
return c
}
// #18 POST /login/password and #19 POST /signup/password.
func TestPasswordSignupAndLogin(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
// #19 signup, invalid (short password) → back to the form with an error.
bad := post(t, ts, h.auth, "/signup/password", url.Values{"username": {"pwuser"}, "password": {"short"}})
bad.Body.Close()
if loc, _ := url.Parse(bad.Header.Get("Location")); bad.StatusCode != http.StatusSeeOther || loc.Path != "/signup" || loc.Query().Get("error") == "" {
t.Fatalf("bad signup = %d %q, want 303 /signup?error", bad.StatusCode, bad.Header.Get("Location"))
}
// #19 signup, valid → creates the account + an SSO session and hands off.
ok := post(t, ts, h.auth, "/signup/password", url.Values{"username": {"pwuser"}, "password": {"supersecret"}})
ok.Body.Close()
if ok.StatusCode != http.StatusSeeOther || cookieByName(ok, "meshtender_session") == nil {
t.Fatalf("valid signup = %d, session=%v; want 303 + session cookie", ok.StatusCode, cookieByName(ok, "meshtender_session"))
}
if _, err := st.GetUserByUsername(ctx, "pwuser"); err != nil {
t.Fatalf("signup did not create the user: %v", err)
}
// #18 login, wrong password → back to the form with an error.
wrong := post(t, ts, h.auth, "/login/password", url.Values{"username": {"pwuser"}, "password": {"wrongpass"}})
wrong.Body.Close()
if loc, _ := url.Parse(wrong.Header.Get("Location")); wrong.StatusCode != http.StatusSeeOther || loc.Path != "/login" || loc.Query().Get("error") == "" {
t.Fatalf("wrong login = %d %q, want 303 /login?error", wrong.StatusCode, wrong.Header.Get("Location"))
}
// #18 login, correct credentials → session + handoff.
good := post(t, ts, h.auth, "/login/password", url.Values{"username": {"pwuser"}, "password": {"supersecret"}})
good.Body.Close()
if good.StatusCode != http.StatusSeeOther || cookieByName(good, "meshtender_session") == nil {
t.Fatalf("good login = %d, session=%v; want 303 + session cookie", good.StatusCode, cookieByName(good, "meshtender_session"))
}
}
// assertAccountOK asserts a 303 back to /account carrying a success ("ok") flash.
func assertAccountOK(t *testing.T, resp *http.Response, label string) {
t.Helper()
loc, _ := url.Parse(resp.Header.Get("Location"))
if resp.StatusCode != http.StatusSeeOther || loc.Path != "/account" {
t.Fatalf("%s: %d %q, want 303 → /account", label, resp.StatusCode, resp.Header.Get("Location"))
}
if loc.Query().Get("ok") == "" {
t.Fatalf("%s: %q, want an ok flash", label, resp.Header.Get("Location"))
}
}
// #29 profile, #100 profile-fields, #101 links, #30 password, #28 username.
func TestAccountProfilePosts(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
sso := authSSO(t, ts, h, "acctuser")
profile := post(t, ts, h.auth, "/account/profile", url.Values{"display_name": {"Ada Lovelace"}}, sso)
profile.Body.Close()
assertAccountOK(t, profile, "profile")
fields := post(t, ts, h.auth, "/account/profile-fields",
url.Values{"bio": {"hi there"}, "location": {"NYC"}, "callsign": {"W1AW"}}, sso)
fields.Body.Close()
assertAccountOK(t, fields, "profile-fields")
links := post(t, ts, h.auth, "/account/links",
url.Values{"link_platform": {"website"}, "link_label": {"Site"}, "link_url": {"https://example.com"}}, sso)
links.Body.Close()
assertAccountOK(t, links, "links")
pw := post(t, ts, h.auth, "/account/password",
url.Values{"new_password": {"anothersecret"}, "current_password": {"supersecret"}}, sso)
pw.Body.Close()
assertAccountOK(t, pw, "password")
// Username change last (it changes the identity). A fresh account has no prior
// self-change, so the 30-day cooldown doesn't apply.
uname := post(t, ts, h.auth, "/account/username", url.Values{"username": {"renamed-acct"}}, sso)
uname.Body.Close()
assertAccountOK(t, uname, "username")
if _, err := st.GetUserByUsername(ctx, "renamed-acct"); err != nil {
t.Fatalf("username was not changed: %v", err)
}
}
// #31 passkey rename, #32 passkey delete. The user has no passkeys, but the
// endpoints must still resolve cleanly back to /account (they don't render).
func TestAccountPasskeyPosts(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
sso := authSSO(t, ts, h, "pkuser")
rn := post(t, ts, h.auth, "/account/passkeys/rename",
url.Values{"credential_id": {"1"}, "name": {"Phone"}}, sso)
rn.Body.Close()
if loc, _ := url.Parse(rn.Header.Get("Location")); rn.StatusCode != http.StatusSeeOther || loc.Path != "/account" {
t.Fatalf("passkey rename = %d %q, want 303 → /account", rn.StatusCode, rn.Header.Get("Location"))
}
// The user still has a password, so removing a (nonexistent) passkey is allowed.
del := post(t, ts, h.auth, "/account/passkeys/delete", url.Values{"credential_id": {"1"}}, sso)
del.Body.Close()
if loc, _ := url.Parse(del.Header.Get("Location")); del.StatusCode != http.StatusSeeOther || loc.Path != "/account" {
t.Fatalf("passkey delete = %d %q, want 303 → /account", del.StatusCode, del.Header.Get("Location"))
}
}
+138
View File
@@ -0,0 +1,138 @@
package core
import (
"net/http"
"net/url"
"strconv"
"strings"
"testing"
)
// Black-box coverage for the org-management POST endpoints (create/edit/links/
// members/my-commands and join/leave). Each asserts the 303 redirect target and a
// cheap store side-effect; none render anything.
// #43 create, #44 edit, #104 links, #48 member role, #50 my-commands.
func TestOrgManagementPosts(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
_, sess := appLogin(t, ts, st, ctx, h.app, "orgadmin")
// #43 create → 303 to the new org's page.
create := post(t, ts, h.app, "/orgs", url.Values{"name": {"Test Org"}}, sess)
create.Body.Close()
loc, _ := url.Parse(create.Header.Get("Location"))
if create.StatusCode != http.StatusSeeOther || !strings.HasPrefix(loc.Path, "/orgs/") {
t.Fatalf("create org = %d %q, want 303 → /orgs/{slug}", create.StatusCode, create.Header.Get("Location"))
}
slug := strings.TrimPrefix(loc.Path, "/orgs/")
// #44 edit (renames the slug) → 303 to the new canonical URL.
edit := post(t, ts, h.app, "/orgs/"+slug+"/edit",
url.Values{"name": {"Renamed Org"}, "slug": {"renamed-org"}, "description": {"desc"}, "region": {"NA"}}, sess)
edit.Body.Close()
if loc, _ := url.Parse(edit.Header.Get("Location")); edit.StatusCode != http.StatusSeeOther || loc.Path != "/orgs/renamed-org" {
t.Fatalf("edit org = %d %q, want 303 → /orgs/renamed-org", edit.StatusCode, edit.Header.Get("Location"))
}
slug = "renamed-org"
// #104 links → 303 back to the org page.
links := post(t, ts, h.app, "/orgs/"+slug+"/links",
url.Values{"link_platform": {"website"}, "link_label": {"Home"}, "link_url": {"https://example.org"}}, sess)
links.Body.Close()
if loc, _ := url.Parse(links.Header.Get("Location")); links.StatusCode != http.StatusSeeOther || loc.Path != "/orgs/"+slug {
t.Fatalf("set org links = %d %q, want 303 → /orgs/%s", links.StatusCode, links.Header.Get("Location"), slug)
}
// #48 member role — promote a second member to admin.
orgID, ok := func() (int64, bool) { id, err := st.OrgIDBySlug(ctx, slug); return id, err == nil }()
if !ok {
t.Fatal("could not resolve renamed org slug")
}
other, err := st.CreateUser(ctx, "orgmember", "")
if err != nil {
t.Fatal(err)
}
if err := st.AddOrgMember(ctx, orgID, other.ID, "member"); err != nil {
t.Fatal(err)
}
promote := post(t, ts, h.app, "/orgs/"+slug+"/members/"+strconv.FormatInt(other.ID, 10),
url.Values{"action": {"promote"}}, sess)
promote.Body.Close()
if loc, _ := url.Parse(promote.Header.Get("Location")); promote.StatusCode != http.StatusSeeOther || loc.Path != "/orgs/"+slug+"/members" {
t.Fatalf("promote member = %d %q, want 303 → members", promote.StatusCode, promote.Header.Get("Location"))
}
if admin, _ := st.IsOrgAdmin(ctx, orgID, other.ID); !admin {
t.Fatal("promote did not make the member an admin")
}
// #50 my-commands — "clear" removes any restriction, redirecting to the editor.
cmds := post(t, ts, h.app, "/orgs/"+slug+"/my-commands", url.Values{"clear": {"1"}}, sess)
cmds.Body.Close()
if loc, _ := url.Parse(cmds.Header.Get("Location")); cmds.StatusCode != http.StatusSeeOther || loc.Path != "/orgs/"+slug+"/my-commands" {
t.Fatalf("save my-commands = %d %q, want 303 → my-commands", cmds.StatusCode, cmds.Header.Get("Location"))
}
}
// #56 update config profile, #57 delete config profile. (Create #54 and regions
// #59 are covered by TestOrgConfigProfilesFlow.)
func TestOrgConfigProfileUpdateDelete(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
owner, sess := appLogin(t, ts, st, ctx, h.app, "cfgadmin")
org, err := st.CreateOrg(ctx, "Cfg Org", owner.ID)
if err != nil {
t.Fatal(err)
}
pid, err := st.CreateProfile(ctx, org.ID, "ESP32", nil)
if err != nil {
t.Fatal(err)
}
base := "/orgs/" + org.Slug + "/config"
upd := post(t, ts, h.app, base+"/profiles/"+strconv.FormatInt(pid, 10),
url.Values{"profile_name": {"Heltec"}, "profile_steps": {"# base"}}, sess)
upd.Body.Close()
if loc, _ := url.Parse(upd.Header.Get("Location")); upd.StatusCode != http.StatusSeeOther || loc.Path != base+"/edit" {
t.Fatalf("update profile = %d %q, want 303 → config/edit", upd.StatusCode, upd.Header.Get("Location"))
}
del := post(t, ts, h.app, base+"/profiles/"+strconv.FormatInt(pid, 10)+"/delete", url.Values{}, sess)
del.Body.Close()
if loc, _ := url.Parse(del.Header.Get("Location")); del.StatusCode != http.StatusSeeOther || loc.Path != base+"/edit" {
t.Fatalf("delete profile = %d %q, want 303 → config/edit", del.StatusCode, del.Header.Get("Location"))
}
}
// #46 join, #47 leave — against an org owned by someone else.
func TestOrgJoinLeavePosts(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
owner, err := st.CreateUser(ctx, "otherowner", "")
if err != nil {
t.Fatal(err)
}
org, err := st.CreateOrg(ctx, "Joinable Org", owner.ID)
if err != nil {
t.Fatal(err)
}
joiner, sess := appLogin(t, ts, st, ctx, h.app, "joiner")
join := post(t, ts, h.app, "/orgs/"+org.Slug+"/join", url.Values{}, sess)
join.Body.Close()
if loc, _ := url.Parse(join.Header.Get("Location")); join.StatusCode != http.StatusSeeOther || loc.Path != "/orgs/"+org.Slug {
t.Fatalf("join = %d %q, want 303 → /orgs/%s", join.StatusCode, join.Header.Get("Location"), org.Slug)
}
if _, isMember, _ := st.OrgRole(ctx, org.ID, joiner.ID); !isMember {
t.Fatal("join did not add membership")
}
leave := post(t, ts, h.app, "/orgs/"+org.Slug+"/leave", url.Values{}, sess)
leave.Body.Close()
if loc, _ := url.Parse(leave.Header.Get("Location")); leave.StatusCode != http.StatusSeeOther || loc.Path != "/orgs" {
t.Fatalf("leave = %d %q, want 303 → /orgs", leave.StatusCode, leave.Header.Get("Location"))
}
if _, isMember, _ := st.OrgRole(ctx, org.ID, joiner.ID); isMember {
t.Fatal("leave did not remove membership")
}
}
+213
View File
@@ -0,0 +1,213 @@
package core
import (
"context"
"crypto/rand"
"net/http"
"net/url"
"strconv"
"testing"
meshcore "github.com/meshcore-go/meshcore-go"
"github.com/jleight/meshtender/internal/store"
)
// Black-box coverage for the repeater, sharing, invite, and admin POST endpoints.
// All redirect (303) and do not render; each test asserts the redirect target and,
// where cheap, a store side-effect.
// newOwnedRepeater creates a repeater owned by ownerID with a valid MeshCore key.
func newOwnedRepeater(t *testing.T, st *store.Store, ctx context.Context, ownerID int64, name string) *store.Repeater {
t.Helper()
id, err := meshcore.GenerateLocalIdentity(rand.Reader)
if err != nil {
t.Fatalf("generate identity: %v", err)
}
rep, err := st.CreateRepeater(ctx, &store.Repeater{
OwnerID: ownerID, Name: name, PublicKeyHex: id.String(),
RadioFreqHz: 869525000, RadioBwHz: 250000, RadioSF: 11, RadioCR: 5,
})
if err != nil {
t.Fatalf("create repeater: %v", err)
}
return rep
}
// assertRedirect asserts resp is a 303 whose Location path equals want.
func assertRedirect(t *testing.T, resp *http.Response, want, label string) {
t.Helper()
loc, _ := url.Parse(resp.Header.Get("Location"))
if resp.StatusCode != http.StatusSeeOther || loc.Path != want {
t.Fatalf("%s = %d %q, want 303 → %s", label, resp.StatusCode, resp.Header.Get("Location"), want)
}
}
// #63 create, #67 edit, #76 docs, #78 add + #79 delete maintenance, #69 delete.
func TestRepeaterCrudPosts(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
owner, sess := appLogin(t, ts, st, ctx, h.app, "reptester")
// #63 create → /repeaters/{id}/added
id, _ := meshcore.GenerateLocalIdentity(rand.Reader)
create := post(t, ts, h.app, "/repeaters", url.Values{
"name": {"New Rep"}, "public_key": {id.String()},
"radio_freq_mhz": {"869.525"}, "radio_bw_khz": {"250"}, "radio_sf": {"11"}, "radio_cr": {"5"},
}, sess)
create.Body.Close()
loc, _ := url.Parse(create.Header.Get("Location"))
if create.StatusCode != http.StatusSeeOther || loc.Path == "" || loc.Path[len(loc.Path)-6:] != "/added" {
t.Fatalf("create repeater = %d %q, want 303 → /repeaters/{id}/added", create.StatusCode, create.Header.Get("Location"))
}
rep := newOwnedRepeater(t, st, ctx, owner.ID, "Edit Me")
pid := rep.PublicID
edit := post(t, ts, h.app, "/repeaters/"+pid+"/edit", url.Values{
"name": {"Edited"}, "radio_freq_mhz": {"869.525"}, "radio_bw_khz": {"250"}, "radio_sf": {"11"}, "radio_cr": {"5"},
}, sess)
edit.Body.Close()
assertRedirect(t, edit, "/", "edit repeater")
docs := post(t, ts, h.app, "/repeaters/"+pid+"/docs",
url.Values{"doc_public": {"public notes"}, "doc_internal": {"internal notes"}}, sess)
docs.Body.Close()
assertRedirect(t, docs, "/repeaters/"+pid+"/docs", "save docs")
maint := post(t, ts, h.app, "/repeaters/"+pid+"/maintenance", url.Values{"note": {"swapped antenna"}}, sess)
maint.Body.Close()
assertRedirect(t, maint, "/repeaters/"+pid+"/maintenance", "add maintenance")
entries, err := st.ListMaintenance(ctx, rep.ID)
if err != nil || len(entries) == 0 {
t.Fatalf("list maintenance: %v (n=%d)", err, len(entries))
}
del := post(t, ts, h.app, "/repeaters/"+pid+"/maintenance/delete",
url.Values{"entry_id": {strconv.FormatInt(entries[0].ID, 10)}}, sess)
del.Body.Close()
assertRedirect(t, del, "/repeaters/"+pid+"/maintenance", "delete maintenance")
delRep := post(t, ts, h.app, "/repeaters/"+pid+"/delete", url.Values{}, sess)
delRep.Body.Close()
assertRedirect(t, delRep, "/", "delete repeater")
}
// #81 create link, #82 delete link, #85 share commands, #86 steward, #83 unshare,
// #87 org participation.
func TestRepeaterSharePosts(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
owner, sess := appLogin(t, ts, st, ctx, h.app, "shareowner")
rep := newOwnedRepeater(t, st, ctx, owner.ID, "Shared Rep")
pid := rep.PublicID
share := "/repeaters/" + pid + "/share"
link := post(t, ts, h.app, share+"/link", url.Values{"description": {"friends"}}, sess)
link.Body.Close()
assertRedirect(t, link, share, "create share link")
invites, err := st.ListInvites(ctx, rep.ID)
if err != nil || len(invites) == 0 {
t.Fatalf("list invites: %v (n=%d)", err, len(invites))
}
dl := post(t, ts, h.app, share+"/link/delete", url.Values{"invite_id": {strconv.FormatInt(invites[0].ID, 10)}}, sess)
dl.Body.Close()
assertRedirect(t, dl, share, "delete share link")
// A directly-added share to a second user, for the per-share endpoints.
target, err := st.CreateUser(ctx, "sharee", "")
if err != nil {
t.Fatal(err)
}
if _, err := st.AddShare(ctx, rep.ID, target.ID); err != nil {
t.Fatal(err)
}
tid := strconv.FormatInt(target.ID, 10)
cmds := post(t, ts, h.app, share+"/"+tid+"/commands", url.Values{}, sess) // empty = no commands
cmds.Body.Close()
assertRedirect(t, cmds, share, "set share commands")
stew := post(t, ts, h.app, share+"/"+tid+"/steward", url.Values{"steward": {"1"}}, sess)
stew.Body.Close()
assertRedirect(t, stew, share, "set steward")
un := post(t, ts, h.app, "/repeaters/"+pid+"/unshare", url.Values{"user_id": {tid}}, sess)
un.Body.Close()
assertRedirect(t, un, share, "unshare")
// #87 participation: exclude this repeater from an org the owner belongs to.
org, err := st.CreateOrg(ctx, "Participation Org", owner.ID)
if err != nil {
t.Fatal(err)
}
// The {orgID} route param is the org slug, not the numeric id.
part := post(t, ts, h.app, "/repeaters/"+pid+"/orgs/"+org.Slug+"/participation",
url.Values{"action": {"exclude"}}, sess)
part.Body.Close()
assertRedirect(t, part, share, "org participation")
}
// #88 POST /invite/{token}/accept — a second user redeems a share link.
func TestAcceptInvitePost(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
owner, err := st.CreateUser(ctx, "invowner", "")
if err != nil {
t.Fatal(err)
}
rep := newOwnedRepeater(t, st, ctx, owner.ID, "Invite Rep")
token, err := st.CreateInvite(ctx, rep.ID, "come join")
if err != nil {
t.Fatal(err)
}
invitee, sess := appLogin(t, ts, st, ctx, h.app, "invitee")
acc := post(t, ts, h.app, "/invite/"+token+"/accept", url.Values{}, sess)
acc.Body.Close()
assertRedirect(t, acc, "/", "accept invite")
// The invitee now sees the repeater in their list (shared).
reps, err := st.ListRepeatersForUser(ctx, invitee.ID)
if err != nil {
t.Fatal(err)
}
found := false
for _, rp := range reps {
if rp.ID == rep.ID {
found = true
}
}
if !found {
t.Fatal("accepting the invite did not share the repeater with the invitee")
}
}
// #92 catalog update, #95 set user capabilities. Both require an admin cap, which
// the session picks up live from the store on the next request.
func TestAdminPosts(t *testing.T) {
t.Parallel()
st, ctx, ts, h := splitServer(t)
admin, sess := appLogin(t, ts, st, ctx, h.app, "superadmin")
if err := st.SetCapabilities(ctx, admin.ID, true, true); err != nil {
t.Fatal(err)
}
cmds, err := st.ListCommands(ctx)
if err != nil || len(cmds) == 0 {
t.Fatalf("list commands: %v (n=%d)", err, len(cmds))
}
cat := post(t, ts, h.app, "/admin/catalog/"+strconv.FormatInt(cmds[0].ID, 10), url.Values{}, sess)
cat.Body.Close()
assertRedirect(t, cat, "/admin/catalog", "catalog update")
target, err := st.CreateUser(ctx, "capuser", "")
if err != nil {
t.Fatal(err)
}
setCaps := post(t, ts, h.app, "/admin/users/"+strconv.FormatInt(target.ID, 10),
url.Values{"manage_catalog": {"1"}}, sess)
setCaps.Body.Close()
assertRedirect(t, setCaps, "/admin/users", "set user caps")
}
+111
View File
@@ -0,0 +1,111 @@
package core
import (
"io"
"net/http"
"net/url"
"strings"
"testing"
)
// These black-box tests cover the non-visual "plumbing" endpoints from the
// endpoint inventory (docs/endpoint-inventory.md): health, static assets, and the
// pure host/redirect behaviors. They use the splitServer harness (real three-host
// server + real Postgres) and assert status/redirect Location rather than any
// rendered UI.
// #1 /healthz — returns "ok" on every surface.
func TestHealthzEndpoint(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
for _, host := range []string{h.app, h.auth, h.root} {
resp := do(t, ts, host, "/healthz")
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK || strings.TrimSpace(string(body)) != "ok" {
t.Fatalf("%s/healthz = %d %q, want 200 \"ok\"", host, resp.StatusCode, body)
}
}
}
// #2 /static/* — serves an embedded asset.
func TestStaticAssetEndpoint(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
resp := do(t, ts, h.app, "/static/ui.js")
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("/static/ui.js = %d, want 200", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "javascript") {
t.Fatalf("/static/ui.js Content-Type = %q, want a javascript type", ct)
}
if len(body) == 0 {
t.Fatal("/static/ui.js served an empty body")
}
}
// #15 auth host `/` — bare visits 303 to the sign-in page.
func TestAuthRootRedirectsToLogin(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
resp := do(t, ts, h.auth, "/")
resp.Body.Close()
if resp.StatusCode != http.StatusSeeOther {
t.Fatalf("auth / = %d, want 303", resp.StatusCode)
}
if loc, _ := url.Parse(resp.Header.Get("Location")); loc.Path != "/login" {
t.Fatalf("auth / → %q, want /login", resp.Header.Get("Location"))
}
}
// #35 app host `/signup` — starts the signup handoff, bouncing to the auth host.
func TestAppSignupRedirectsToAuth(t *testing.T) {
t.Parallel()
_, _, ts, h := splitServer(t)
resp := do(t, ts, h.app, "/signup")
resp.Body.Close()
if resp.StatusCode != http.StatusSeeOther {
t.Fatalf("app /signup = %d, want 303", resp.StatusCode)
}
if loc, _ := url.Parse(resp.Header.Get("Location")); loc.Host != h.auth || loc.Path != "/signup" {
t.Fatalf("app /signup → %q, want auth host /signup", resp.Header.Get("Location"))
}
}
// #97 custom org domain — a verified domain serves the org's public page at `/`
// and 302-redirects every other path to the app host.
func TestCustomDomainRedirect(t *testing.T) {
t.Parallel()
st, ctx, ts, _ := splitServer(t)
owner, err := st.CreateUser(ctx, "domainowner", "")
if err != nil {
t.Fatal(err)
}
org, err := st.CreateOrg(ctx, "Domain Org", owner.ID)
if err != nil {
t.Fatal(err)
}
const customHost = "mesh.example.org"
dom, err := st.CreateOrgDomain(ctx, org.ID, customHost)
if err != nil {
t.Fatal(err)
}
if err := st.MarkOrgDomainVerified(ctx, org.ID, dom.ID); err != nil {
t.Fatal(err)
}
// Non-root path on the custom host → 302 to the same path on the app host.
resp := do(t, ts, customHost, "/repeaters/abc")
resp.Body.Close()
if resp.StatusCode != http.StatusFound {
t.Fatalf("custom-domain /repeaters/abc = %d, want 302", resp.StatusCode)
}
if loc, _ := url.Parse(resp.Header.Get("Location")); loc.Hostname() != testAppHost || loc.Path != "/repeaters/abc" {
t.Fatalf("custom-domain redirect = %q, want app host /repeaters/abc", resp.Header.Get("Location"))
}
// (The custom-domain `/` org page — inventory #96 — is a rendered page left to
// manual/browser verification.)
}