Files
pages/internal/adminapi/router_test.go
T
2026-08-15 07:13:00 +00:00

142 lines
5.0 KiB
Go

package adminapi
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/iceBear67/simplepages/api"
"github.com/iceBear67/simplepages/internal/auth"
)
// The client builds URLs with api.Path* and the server registers wildcard
// patterns. Nothing in the type system connects the two, so assert that every
// path the client can build actually lands on the route it is meant to.
func TestPathBuildersMatchRoutes(t *testing.T) {
mux := http.NewServeMux()
(&Server{Auth: &auth.Middleware{}}).Register(mux)
cases := []struct {
method, path, want string
}{
{http.MethodGet, api.PathWhoAmI(), "GET " + patWhoAmI},
{http.MethodGet, api.PathSystemInfo(), "GET " + patSystemInfo},
{http.MethodPost, api.PathProjects(), "POST " + patProjects},
{http.MethodGet, api.PathProjects(), "GET " + patProjects},
{http.MethodGet, api.PathProject("demo"), "GET " + patProject},
{http.MethodPatch, api.PathProject("demo"), "PATCH " + patProject},
{http.MethodDelete, api.PathProject("demo"), "DELETE " + patProject},
{http.MethodPost, api.PathProjectKeys("demo"), "POST " + patProjectKeys},
{http.MethodGet, api.PathProjectKeys("demo"), "GET " + patProjectKeys},
{http.MethodPost, api.PathKeys(), "POST " + patKeys},
{http.MethodGet, api.PathKeys(), "GET " + patKeys},
{http.MethodDelete, api.PathKey("abcdefghijklmnop"), "DELETE " + patKey},
}
for _, tc := range cases {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
r := httptest.NewRequest(tc.method, tc.path, nil)
_, pattern := mux.Handler(r)
if pattern != tc.want {
t.Errorf("routed to %q, want %q", pattern, tc.want)
}
})
}
}
// A project name that needs escaping must not be able to reach a different
// route by smuggling a slash through the path builder.
func TestPathEscapingCannotCrossRoutes(t *testing.T) {
mux := http.NewServeMux()
(&Server{Auth: &auth.Middleware{}}).Register(mux)
built := api.PathProject("demo/keys")
if strings.Contains(built, "demo/keys") {
t.Fatalf("PathProject did not escape the slash: %q", built)
}
r := httptest.NewRequest(http.MethodGet, built, nil)
_, pattern := mux.Handler(r)
if pattern != "GET "+patProject {
t.Errorf("routed to %q, want %q", pattern, "GET "+patProject)
}
}
func TestEveryRouteRequiresAuthentication(t *testing.T) {
e := newEnv(t)
p := e.createProject(t, "demo")
cases := []struct{ method, path string }{
{http.MethodGet, api.PathWhoAmI()},
{http.MethodGet, api.PathSystemInfo()},
{http.MethodPost, api.PathProjects()},
{http.MethodGet, api.PathProjects()},
{http.MethodGet, api.PathProject(p.Name)},
{http.MethodPatch, api.PathProject(p.Name)},
{http.MethodDelete, api.PathProject(p.Name)},
{http.MethodPost, api.PathProjectKeys(p.Name)},
{http.MethodGet, api.PathProjectKeys(p.Name)},
{http.MethodPost, api.PathKeys()},
{http.MethodGet, api.PathKeys()},
{http.MethodDelete, api.PathKey("abcdefghijklmnop")},
{http.MethodGet, api.Version + "/does-not-exist"},
}
for _, tc := range cases {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
status, body := e.do(t, tc.method, tc.path, "", nil)
if status != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401; body: %s", status, body)
}
if got := errCode(t, body); got != api.CodeUnauthorized {
t.Errorf("code = %q, want %q", got, api.CodeUnauthorized)
}
})
}
}
// An unknown endpoint must answer with the same envelope as everything else, so
// a client has exactly one error shape to parse.
func TestUnknownEndpointReturnsEnvelope(t *testing.T) {
e := newEnv(t)
status, body := e.do(t, http.MethodGet, api.Version+"/nope", e.adminToken, nil)
if status != http.StatusNotFound {
t.Fatalf("status = %d, want 404; body: %s", status, body)
}
if got := errCode(t, body); got != api.CodeNotFound {
t.Errorf("code = %q, want %q", got, api.CodeNotFound)
}
// The path is not echoed back: there is no reason to reflect caller-supplied
// bytes into a response body.
if bytes := string(body); strings.Contains(bytes, "nope") {
t.Errorf("response echoes the request path: %s", bytes)
}
}
// The catch-all matches every path under the API prefix, so without the
// method-agnostic fallbacks a wrong verb on a real endpoint would report 404
// "no such endpoint" — which sends a client looking for a typo that is not
// there.
func TestWrongMethodIsRejectedWithAllow(t *testing.T) {
e := newEnv(t)
e.createProject(t, "demo")
cases := []struct {
method, path, wantAllow string
}{
{http.MethodPut, api.PathProjects(), "POST"},
{http.MethodPost, api.PathProject("demo"), "PATCH"},
{http.MethodPut, api.PathKey("abcdefghijklmnop"), "DELETE"},
{http.MethodDelete, api.PathWhoAmI(), "GET"},
}
for _, tc := range cases {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
resp := e.doResp(t, tc.method, tc.path, e.adminToken, nil)
if resp.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("status = %d, want 405", resp.StatusCode)
}
if allow := resp.Header.Get("Allow"); !strings.Contains(allow, tc.wantAllow) {
t.Errorf("Allow = %q, want it to mention %s", allow, tc.wantAllow)
}
})
}
}