diff --git a/cyclops/server.go b/cyclops/server.go index 2d3123a..e0adbd1 100644 --- a/cyclops/server.go +++ b/cyclops/server.go @@ -30,9 +30,6 @@ type ModCyclopsServer struct { } func MakeModCyclopsServer(logger *catlogger.Logger, ccmsClient CCMSClient, root string, timeout int) *ModCyclopsServer { - tr := &http.Transport{} - tr.RegisterProtocol("file", http.NewFileTransport(http.Dir(root))) - r := chi.NewRouter() var server = ModCyclopsServer{ logger: logger, diff --git a/cyclops/server_test.go b/cyclops/server_test.go new file mode 100644 index 0000000..4a984f5 --- /dev/null +++ b/cyclops/server_test.go @@ -0,0 +1,153 @@ +package cyclops + +import "encoding/json" +import "net/http" +import "net/http/httptest" +import "os" +import "path/filepath" +import "reflect" +import "testing" +import "github.com/MikeTaylor/catlogger" +import "github.com/indexdata/ccms" + +// serve drives a request through the router the constructor installed on the +// server's http.Server, returning the recorder. This exercises MakeModCyclops- +// Server's routing table rather than calling handlers directly. +func serve(server *ModCyclopsServer, req *http.Request) *httptest.ResponseRecorder { + rr := httptest.NewRecorder() + server.httpServer.Handler.ServeHTTP(rr, req) + return rr +} + +func TestMakeModCyclopsServerWiring(t *testing.T) { + fake := &fakeCCMS{} + server := MakeModCyclopsServer(nil, fake, ".", 60) + + if server == nil { + t.Fatal("MakeModCyclopsServer returned nil") + } + if server.ccmsClient != fake { + t.Error("ccmsClient was not stored on the server") + } + + // The timeout argument is converted from seconds to a Duration on both + // read and write deadlines. + if server.httpServer.ReadTimeout != 60*1e9 { + t.Errorf("ReadTimeout: got %v want 60s", server.httpServer.ReadTimeout) + } + if server.httpServer.WriteTimeout != 60*1e9 { + t.Errorf("WriteTimeout: got %v want 60s", server.httpServer.WriteTimeout) + } + if server.httpServer.Handler == nil { + t.Error("constructor did not install an HTTP handler") + } +} + +// The static routes are self-contained: they touch neither CCMS nor the +// logger, so we can assert their responses directly. +func TestMakeModCyclopsServerStaticRoutes(t *testing.T) { + server := newTestServer(&fakeCCMS{}) + + t.Run("health", func(t *testing.T) { + rr := serve(server, httptest.NewRequest(http.MethodGet, "/admin/health", nil)) + assertStatus(t, rr, http.StatusOK) + assertEqual(t, "health body", rr.Body.String(), "Behold! I live!!\n") + }) + + t.Run("root", func(t *testing.T) { + rr := serve(server, httptest.NewRequest(http.MethodGet, "/", nil)) + assertStatus(t, rr, http.StatusOK) + assertEqual(t, "root content type", rr.Header().Get("Content-Type"), "text/html; charset=utf-8") + }) + + t.Run("unknown path yields 404", func(t *testing.T) { + rr := serve(server, httptest.NewRequest(http.MethodGet, "/no/such/thing", nil)) + assertStatus(t, rr, http.StatusNotFound) + assertEqual(t, "not-found body", rr.Body.String(), "Not Found\n") + }) +} + +// A CCMS-backed route, driven end to end through the router, confirms the +// constructor connects the path to the right handler and that the handler in +// turn talks to the wired-in CCMS client. +func TestMakeModCyclopsServerRoutesToHandler(t *testing.T) { + fake := &fakeCCMS{resp: listResponse("vip", "staff")} + server := newTestServer(fake) + + rr := serve(server, httptest.NewRequest(http.MethodGet, "/cyclops/tags", nil)) + + assertStatus(t, rr, http.StatusOK) + assertEqual(t, "command sent to CCMS", fake.lastCmd, "show tags;") + + var got TagList + err := json.Unmarshal(rr.Body.Bytes(), &got) + if err != nil { + t.Fatalf("could not decode response body %q: %v", rr.Body.String(), err) + } + want := TagList{Tags: []any{"vip", "staff"}} + if !reflect.DeepEqual(got, want) { + t.Errorf("translated response:\n got %+v\nwant %+v", got, want) + } +} + +// serverRootedAt builds a server whose static files are served from the given +// directory. The htdocs FileServer reads from /htdocs. +func serverRootedAt(root string) *ModCyclopsServer { + logger := catlogger.MakeLogger("", "", false) + return MakeModCyclopsServer(logger, &fakeCCMS{}, root, 60) +} + +// writeFixture creates /htdocs/ with the given contents, making the +// htdocs directory as needed, and fails the test if anything goes wrong. +func writeFixture(t *testing.T, root, name, contents string) { + t.Helper() + path := filepath.Join(root, "htdocs", name) + err := os.MkdirAll(filepath.Dir(path), 0o755) + if err != nil { + t.Fatalf("could not create fixture dir: %v", err) + } + err = os.WriteFile(path, []byte(contents), 0o644) + if err != nil { + t.Fatalf("could not write fixture %q: %v", path, err) + } +} + +// The /htdocs/* route strips the prefix and serves the named file from the +// htdocs directory under root. +func TestMakeModCyclopsServerHtdocs(t *testing.T) { + root := t.TempDir() + writeFixture(t, root, "hello.txt", "hello, world\n") + server := serverRootedAt(root) + + t.Run("serves an existing file", func(t *testing.T) { + rr := serve(server, httptest.NewRequest(http.MethodGet, "/htdocs/hello.txt", nil)) + assertStatus(t, rr, http.StatusOK) + assertEqual(t, "file body", rr.Body.String(), "hello, world\n") + }) + + t.Run("missing file yields 404", func(t *testing.T) { + rr := serve(server, httptest.NewRequest(http.MethodGet, "/htdocs/nope.txt", nil)) + assertStatus(t, rr, http.StatusNotFound) + }) +} + +// The /favicon.ico route is handled by the same FileServer but without a +// StripPrefix, so it serves htdocs/favicon.ico directly. +func TestMakeModCyclopsServerFavicon(t *testing.T) { + root := t.TempDir() + writeFixture(t, root, "favicon.ico", "icon-bytes") + server := serverRootedAt(root) + + rr := serve(server, httptest.NewRequest(http.MethodGet, "/favicon.ico", nil)) + assertStatus(t, rr, http.StatusOK) + assertEqual(t, "favicon body", rr.Body.String(), "icon-bytes") +} + +// A registered path that is hit with the wrong method should not fall through +// to the generic NotFound handler; chi answers 405 itself. +func TestMakeModCyclopsServerMethodNotAllowed(t *testing.T) { + server := newTestServer(&fakeCCMS{resp: ccms.NewResponse()}) + + rr := serve(server, httptest.NewRequest(http.MethodPatch, "/cyclops/tags", nil)) + assertStatus(t, rr, http.StatusMethodNotAllowed) +}