docs: add releasing process documentation

chore: update go.mod dependencies and versions

chore: update go.sum with new dependency versions

scripts: add coverage check script

scripts: add release notes generation script

test: refactor tests to use goconvey for assertions

test: enhance webserver tests with additional cases and goconvey

fix: improve error logging in web server start and stop methods
This commit is contained in:
2026-03-29 20:47:30 +02:00
parent d5660ba8ca
commit 9b0d844ec3
11 changed files with 794 additions and 206 deletions
+75 -57
View File
@@ -3,7 +3,7 @@ package test
import (
"testing"
"github.com/stretchr/testify/assert"
. "github.com/smartystreets/goconvey/convey"
cfg "scm.yoorie.de/go-lib/micro/config"
)
@@ -13,70 +13,88 @@ type MyConfig struct {
Value2 int `default:"8080" yaml:"value2"`
}
func (config *MyConfig) appName() string {
func (config *MyConfig) AppName() string {
return "myconfig"
}
func TestIsStructPointerStruct(t *testing.T) {
assert.False(t, cfg.IsStructPointer(MyConfig{}))
}
func TestIsStructPointerString(t *testing.T) {
assert.False(t, cfg.IsStructPointer("Bar"))
}
func TestIsStructPointerSimpleArray(t *testing.T) {
assert.False(t, cfg.IsStructPointer([2]int{292, 2}))
}
func TestIsStructPointerInt(t *testing.T) {
assert.False(t, cfg.IsStructPointer(12))
}
func TestIsStructPointerBool(t *testing.T) {
assert.False(t, cfg.IsStructPointer(false))
}
func TestIsStructPointerStructPointer(t *testing.T) {
assert.True(t, cfg.IsStructPointer(&MyConfig{}))
}
func TestIsStructPointerStructPointerArray(t *testing.T) {
cfg1 := &MyConfig{
Value: "Foo",
}
cfg2 := &MyConfig{
Value: "Bar",
}
array := [2]*MyConfig{cfg1, cfg2}
assert.False(t, cfg.IsStructPointer(array))
}
func TestLoadConfigurationFromFileWithNoPointerInvalidType(t *testing.T) {
myConfig := MyConfig{}
err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config.yml")
assert.NotNil(t, err)
func TestIsStructPointer(t *testing.T) {
Convey("IsStructPointer", t, func() {
Convey("returns false for struct value", func() {
So(cfg.IsStructPointer(MyConfig{}), ShouldBeFalse)
})
Convey("returns false for string", func() {
So(cfg.IsStructPointer("Bar"), ShouldBeFalse)
})
Convey("returns false for simple array", func() {
So(cfg.IsStructPointer([2]int{292, 2}), ShouldBeFalse)
})
Convey("returns false for int", func() {
So(cfg.IsStructPointer(12), ShouldBeFalse)
})
Convey("returns false for bool", func() {
So(cfg.IsStructPointer(false), ShouldBeFalse)
})
Convey("returns true for struct pointer", func() {
So(cfg.IsStructPointer(&MyConfig{}), ShouldBeTrue)
})
Convey("returns false for struct pointer array", func() {
cfg1 := &MyConfig{Value: "Foo"}
cfg2 := &MyConfig{Value: "Bar"}
array := [2]*MyConfig{cfg1, cfg2}
So(cfg.IsStructPointer(array), ShouldBeFalse)
})
})
}
func TestLoadConfigurationFromFile(t *testing.T) {
myConfig := &MyConfig{}
assert.Equal(t, "myconfig", myConfig.appName())
cfg.LoadConfigurationFromFile(myConfig, "testdata/config.yml")
assert.Equal(t, "Foo", myConfig.Value)
assert.Equal(t, true, myConfig.Value1)
assert.Equal(t, 8080, myConfig.Value2)
}
func TestLoadConfigurationFromFileNotExist(t *testing.T) {
myConfig := &MyConfig{}
assert.Equal(t, "myconfig", myConfig.appName())
err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config1.yml")
assert.NotNil(t, err)
Convey("LoadConfigurationFromFile", t, func() {
Convey("returns error for non-pointer type", func() {
err := cfg.LoadConfigurationFromFile(MyConfig{}, "testdata/config.yml")
So(err, ShouldNotBeNil)
})
Convey("loads config from file successfully", func() {
myConfig := &MyConfig{}
err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config.yml")
So(err, ShouldBeNil)
So(myConfig.Value, ShouldEqual, "Foo")
So(myConfig.Value1, ShouldBeTrue)
So(myConfig.Value2, ShouldEqual, 8080)
})
Convey("returns error when config file does not exist", func() {
myConfig := &MyConfig{}
err := cfg.LoadConfigurationFromFile(myConfig, "testdata/config1.yml")
So(err, ShouldNotBeNil)
})
})
}
func TestGetConfigurationFiles(t *testing.T) {
fileNames := cfg.GetConfigurationFiles("myapp")
assert.Equal(t, 4, len(fileNames))
Convey("GetConfigurationFiles returns 4 candidate paths", t, func() {
fileNames := cfg.GetConfigurationFiles("myapp")
So(len(fileNames), ShouldEqual, 4)
})
}
for _, fileName := range fileNames {
t.Log(fileName)
}
func TestGetConfigurationFile(t *testing.T) {
Convey("GetConfigurationFile", t, func() {
Convey("returns empty string when no matching config file exists", func() {
result := cfg.GetConfigurationFile("nonexistentapp_xyz_123")
So(result, ShouldEqual, "")
})
})
}
func TestLoadConfiguration(t *testing.T) {
Convey("LoadConfiguration", t, func() {
Convey("returns error for non-pointer type", func() {
err := cfg.LoadConfiguration(MyConfig{})
So(err, ShouldNotBeNil)
})
Convey("returns nil for struct pointer with no config file found", func() {
myConfig := &MyConfig{}
err := cfg.LoadConfiguration(myConfig)
So(err, ShouldBeNil)
So(myConfig.Value, ShouldEqual, "Bar")
})
})
}
+165 -46
View File
@@ -5,15 +5,15 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/stretchr/testify/assert"
. "github.com/smartystreets/goconvey/convey"
"scm.yoorie.de/go-lib/micro/web"
"scm.yoorie.de/go-lib/util"
)
@@ -24,6 +24,12 @@ var (
sslPort int
)
const (
readyMessage = "service is ready"
aMessage = "A message"
testFailureMsg = "test failure"
)
type MyData struct {
Message string `json:"message"`
Count int `json:"count"`
@@ -32,7 +38,7 @@ type MyData struct {
func MyTestEndpoint(w http.ResponseWriter, r *http.Request) {
internalCount++
render.JSON(w, r, &MyData{
Message: "A message",
Message: aMessage,
Count: internalCount,
})
}
@@ -91,7 +97,9 @@ func TestMain(m *testing.M) {
panic(err)
}
server.Mount("/api", CreateTestRouter())
server.Start()
if err = server.Start(); err != nil {
panic(err)
}
// Allow insecure calls to https
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
@@ -101,56 +109,167 @@ func TestMain(m *testing.M) {
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) {
uri := getServerURL(false, "/readyz")
resp, err := http.Get(uri)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
healthData := web.HealthData{}
err = json.Unmarshal(body, &healthData)
assert.Nil(t, err)
assert.Equal(t, "service is ready", healthData.Message)
assert.NotEmpty(t, healthData.LastChecked)
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) {
uri := getServerURL(true, "/health/readyz")
resp, err := http.Get(uri)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
healthData := web.HealthData{}
err = json.Unmarshal(body, &healthData)
assert.Nil(t, err)
assert.Equal(t, "service is ready", healthData.Message)
assert.NotEmpty(t, healthData.LastChecked)
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) {
resp, err := http.Get(getServerURL(false, "/healthz"))
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
healthData := web.HealthData{}
err = json.Unmarshal(body, &healthData)
assert.Nil(t, err)
assert.Equal(t, "service up and running", healthData.Message)
assert.NotEmpty(t, healthData.LastChecked)
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) {
uri := getServerURL(true, "/api/myendpoint")
resp, err := http.Get(uri)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
t.Logf("Body: %s", string(body))
myData := MyData{}
err = json.Unmarshal(body, &myData)
assert.Nil(t, err)
assert.NotNil(t, myData)
assert.Equal(t, "A message", myData.Message)
assert.Greater(t, myData.Count, 0)
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)
})
}