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