142 lines
5.0 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|