package test import ( "crypto/tls" "encoding/json" "fmt" "io" "net" "net/http" "os" "testing" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/render" . "github.com/smartystreets/goconvey/convey" "scm.yoorie.de/go-lib/micro/web" "scm.yoorie.de/go-lib/util" ) var ( internalCount int httpPort int sslPort int ) const ( readyMessage = "service is ready" aMessage = "A message" testFailureMsg = "test failure" ) type MyData struct { Message string `json:"message"` Count int `json:"count"` } func MyTestEndpoint(w http.ResponseWriter, r *http.Request) { internalCount++ render.JSON(w, r, &MyData{ Message: aMessage, Count: internalCount, }) } func CreateTestRouter() *chi.Mux { router := chi.NewRouter() router.Get("/myendpoint", MyTestEndpoint) return router } // GetFreePort asks the kernel for a free open port that is ready to use. func GetFreePort() (int, error) { addr, err := net.ResolveTCPAddr("tcp", "localhost:0") if err != nil { return 0, err } l, err := net.ListenTCP("tcp", addr) if err != nil { return 0, err } defer l.Close() return l.Addr().(*net.TCPAddr).Port, nil } func getServerURL(ssl bool, path string) string { var scheme string var port int if ssl { scheme = "https" port = sslPort } else { scheme = "http" port = httpPort } return util.JoiningSlash(fmt.Sprintf("%s://localhost:%d", scheme, port), path) } func TestMain(m *testing.M) { internalCount = 0 var err error httpPort, err = GetFreePort() if err != nil { panic(err) } sslPort, err = GetFreePort() if err != nil { panic(err) } config := &web.WebServerConfiguration{ Port: httpPort, SslPort: sslPort, } server, err := web.NewWebServer(config) if err != nil { panic(err) } server.Mount("/api", CreateTestRouter()) if err = server.Start(); err != nil { panic(err) } // Allow insecure calls to https http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} exitVal := m.Run() server.Stop() os.Exit(exitVal) } func TestNewWebServerNilConfig(t *testing.T) { Convey("NewWebServer with nil config returns error", t, func() { server, err := web.NewWebServer(nil) So(err, ShouldNotBeNil) So(server, ShouldBeNil) }) } func TestGetBindings(t *testing.T) { Convey("WebServerConfiguration.GetBindings", t, func() { Convey("returns only http binding when SSL port is 0", func() { config := &web.WebServerConfiguration{Port: 8080} So(config.GetBindings(), ShouldEqual, "http://0.0.0.0:8080") }) Convey("returns https and http bindings when SSL port is set", func() { config := &web.WebServerConfiguration{Port: 8080, SslPort: 8443} So(config.GetBindings(), ShouldEqual, "https://0.0.0.0:8443,http://0.0.0.0:8080") }) }) } func TestBuildHostAddress(t *testing.T) { Convey("WebServerConfiguration.BuildHostAddress", t, func() { Convey("uses 0.0.0.0 when host is empty", func() { config := &web.WebServerConfiguration{Port: 8080, SslPort: 8443} So(config.BuildHostAddress(false), ShouldEqual, "0.0.0.0:8080") So(config.BuildHostAddress(true), ShouldEqual, "0.0.0.0:8443") }) Convey("uses configured host when set", func() { config := &web.WebServerConfiguration{Host: "192.168.1.1", Port: 8080, SslPort: 8443} So(config.BuildHostAddress(false), ShouldEqual, "192.168.1.1:8080") So(config.BuildHostAddress(true), ShouldEqual, "192.168.1.1:8443") }) }) } func TestStartWithNoMounts(t *testing.T) { Convey("Start returns error when no mounts are registered", t, func() { port, err := GetFreePort() So(err, ShouldBeNil) config := &web.WebServerConfiguration{Port: port} server, err := web.NewWebServer(config) So(err, ShouldBeNil) err = server.Start() So(err, ShouldNotBeNil) }) } func TestNonSSLServer(t *testing.T) { Convey("Non-SSL web server starts, serves requests, and stops", t, func() { port, err := GetFreePort() So(err, ShouldBeNil) config := &web.WebServerConfiguration{Port: port} server, err := web.NewWebServer(config) So(err, ShouldBeNil) server.Mount("/api", CreateTestRouter()) err = server.Start() So(err, ShouldBeNil) time.Sleep(100 * time.Millisecond) uri := fmt.Sprintf("http://localhost:%d/health/readyz", port) resp, err := http.Get(uri) So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) body, _ := io.ReadAll(resp.Body) healthData := web.HealthData{} err = json.Unmarshal(body, &healthData) So(err, ShouldBeNil) So(healthData.Message, ShouldEqual, readyMessage) server.Stop() }) } func TestUnhealthyServer(t *testing.T) { Convey("Server with failing HealthCheck returns 503 from healthz endpoint", t, func() { port, err := GetFreePort() So(err, ShouldBeNil) config := &web.WebServerConfiguration{Port: port} server, err := web.NewWebServer(config) So(err, ShouldBeNil) server.HealthCheck = func() (bool, string) { return false, testFailureMsg } server.Mount("/api", CreateTestRouter()) err = server.Start() So(err, ShouldBeNil) time.Sleep(100 * time.Millisecond) uri := fmt.Sprintf("http://localhost:%d/health/healthz", port) resp, err := http.Get(uri) So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusServiceUnavailable) server.Stop() }) } func TestReady(t *testing.T) { Convey("HTTP ready endpoint returns 200 with ready message", t, func() { uri := getServerURL(false, "/readyz") resp, err := http.Get(uri) So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) body, _ := io.ReadAll(resp.Body) healthData := web.HealthData{} err = json.Unmarshal(body, &healthData) So(err, ShouldBeNil) So(healthData.Message, ShouldEqual, readyMessage) So(healthData.LastChecked, ShouldNotBeZeroValue) }) } func TestReadySSL(t *testing.T) { Convey("HTTPS ready endpoint returns 200 with ready message", t, func() { uri := getServerURL(true, "/health/readyz") resp, err := http.Get(uri) So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) body, _ := io.ReadAll(resp.Body) healthData := web.HealthData{} err = json.Unmarshal(body, &healthData) So(err, ShouldBeNil) So(healthData.Message, ShouldEqual, readyMessage) So(healthData.LastChecked, ShouldNotBeZeroValue) }) } func TestHealthy(t *testing.T) { Convey("Healthy endpoint returns 200 with running message", t, func() { resp, err := http.Get(getServerURL(false, "/healthz")) So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) body, _ := io.ReadAll(resp.Body) healthData := web.HealthData{} err = json.Unmarshal(body, &healthData) So(err, ShouldBeNil) So(healthData.Message, ShouldEqual, "service up and running") So(healthData.LastChecked, ShouldNotBeZeroValue) }) } func TestSslEndpoint(t *testing.T) { Convey("SSL endpoint returns 200 with expected data", t, func() { uri := getServerURL(true, "/api/myendpoint") resp, err := http.Get(uri) So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) body, err := io.ReadAll(resp.Body) So(err, ShouldBeNil) myData := MyData{} err = json.Unmarshal(body, &myData) So(err, ShouldBeNil) So(myData.Message, ShouldEqual, aMessage) So(myData.Count, ShouldBeGreaterThan, 0) }) }