diff --git a/Taskfile.yml b/Taskfile.yml index e6a15812..320206a6 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -10,4 +10,4 @@ includes: vars: IMAGE_NAME: queryorchestration COVERAGE_THRESHOLD: 80 - FUNCTION_COVERAGE_THRESHOLD: 10 + FUNCTION_COVERAGE_THRESHOLD: 60 diff --git a/internal/server/service/listener.go b/internal/server/service/listener.go index c43dd6db..68a2f820 100644 --- a/internal/server/service/listener.go +++ b/internal/server/service/listener.go @@ -5,7 +5,6 @@ import ( "errors" "log/slog" "net" - "net/http" "strconv" "time" @@ -17,7 +16,6 @@ import ( "github.com/labstack/echo/v4/middleware" _ "github.com/lib/pq" echoSwagger "github.com/swaggo/echo-swagger" - "gopkg.in/yaml.v3" ) type Config interface { @@ -90,7 +88,6 @@ func getSlogCustomEchoMiddleware(logger *slog.Logger) echo.MiddlewareFunc { } func New(ctx context.Context, cfg Config) (*Server, error) { - cleanup, err := server.New(ctx, cfg) if err != nil { return nil, err @@ -114,25 +111,17 @@ func New(ctx context.Context, cfg Config) (*Server, error) { return nil, err } - e.GET("/swagger/doc.json", func(c echo.Context) error { - spec, err := opnapi.MarshalJSON() - if err != nil { - return err - } - return c.Blob(http.StatusOK, "application/json", spec) - }) - e.GET("/swagger/doc.yaml", func(c echo.Context) error { - spec, err := opnapi.MarshalYAML() - if err != nil { - return err - } - yaml, err := yaml.Marshal(spec) - if err != nil { - return err - } + jsonFunc, err := getSwaggerJSONFunc(opnapi) + if err != nil { + return nil, err + } + e.GET("/swagger/doc.json", jsonFunc) - return c.Blob(http.StatusOK, "application/yaml", yaml) - }) + yamlFunc, err := getSwaggerYAMLFunc(opnapi) + if err != nil { + return nil, err + } + e.GET("/swagger/doc.yaml", yamlFunc) e.GET("/swagger/*", echoSwagger.EchoWrapHandler( echoSwagger.DocExpansion("full"), diff --git a/internal/server/service/listener_test.go b/internal/server/service/listener_test.go index 04eeaf0d..240ee8be 100644 --- a/internal/server/service/listener_test.go +++ b/internal/server/service/listener_test.go @@ -2,16 +2,22 @@ package service import ( "context" + "log/slog" + "net/http" + "net/http/httptest" "os" "path" + "strings" "testing" + "queryorchestration/internal/serviceconfig/logger" "queryorchestration/internal/test" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { @@ -78,3 +84,31 @@ func TestGetRouter(t *testing.T) { assert.Equal(t, &echo.Echo{}, r) assert.Equal(t, r, c.Router) } + +func TestGetSlogCustomEchoMiddleware(t *testing.T) { + l := &logger.TestLogger{ + T: t, + } + sl := slog.New(l) + handlerFunc := getSlogCustomEchoMiddleware(sl) + require.NotNil(t, handlerFunc) + + mid := func(c echo.Context) error { + sl.Info("in middleware", "location", "middleware") + return nil + } + myFunc := handlerFunc(mid) + require.NotNil(t, myFunc) + + uri := "/example" + + req := httptest.NewRequest(http.MethodPost, uri, strings.NewReader("hello")) + rec := httptest.NewRecorder() + ctx := echo.New().NewContext(req, rec) + err := myFunc(ctx) + require.Nil(t, err) + + assert.Len(t, l.Logs, 2) + assert.Equal(t, "middleware", l.Logs[0]["location"]) + assert.Equal(t, uri, l.Logs[1]["uri"]) +} diff --git a/internal/server/service/swagger.go b/internal/server/service/swagger.go new file mode 100644 index 00000000..5631cf12 --- /dev/null +++ b/internal/server/service/swagger.go @@ -0,0 +1,35 @@ +package service + +import ( + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/labstack/echo/v4" + "gopkg.in/yaml.v3" +) + +func getSwaggerJSONFunc(opnapi *openapi3.T) (func(echo.Context) error, error) { + spec, err := opnapi.MarshalJSON() + if err != nil { + return nil, err + } + + return func(c echo.Context) error { + return c.Blob(http.StatusOK, "application/json", spec) + }, nil +} + +func getSwaggerYAMLFunc(opnapi *openapi3.T) (func(echo.Context) error, error) { + spec, err := opnapi.MarshalYAML() + if err != nil { + return nil, err + } + yaml, err := yaml.Marshal(spec) + if err != nil { + return nil, err + } + + return func(c echo.Context) error { + return c.Blob(http.StatusOK, "application/yaml", yaml) + }, nil +} diff --git a/internal/server/service/swagger_test.go b/internal/server/service/swagger_test.go new file mode 100644 index 00000000..1a01ec05 --- /dev/null +++ b/internal/server/service/swagger_test.go @@ -0,0 +1,47 @@ +package service + +import ( + "net/http" + "net/http/httptest" + queryservice "queryorchestration/api/queryService" + "strings" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetSwaggerJSONFunc(t *testing.T) { + openapi, err := queryservice.GetSwagger() + require.NoError(t, err) + + f, err := getSwaggerJSONFunc(openapi) + require.NoError(t, err) + require.NotNil(t, f) + + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("hello")) + rec := httptest.NewRecorder() + ctx := echo.New().NewContext(req, rec) + + err = f(ctx) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Result().StatusCode) +} + +func TestGetSwaggerYAMLFunc(t *testing.T) { + openapi, err := queryservice.GetSwagger() + require.NoError(t, err) + + f, err := getSwaggerYAMLFunc(openapi) + require.NoError(t, err) + require.NotNil(t, f) + + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("hello")) + rec := httptest.NewRecorder() + ctx := echo.New().NewContext(req, rec) + + err = f(ctx) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Result().StatusCode) +} diff --git a/internal/serviceconfig/build/build.go b/internal/serviceconfig/build/build.go index dff61dd9..2e651462 100644 --- a/internal/serviceconfig/build/build.go +++ b/internal/serviceconfig/build/build.go @@ -25,10 +25,16 @@ var startupTime time.Time var startupTimeOnce sync.Once func GetVersionTimestamp() time.Time { - startupTimeOnce.Do(func() { - info, ok := debug.ReadBuildInfo() + startupTimeOnce.Do(getStartTime(&startupTime, debug.ReadBuildInfo)) + + return startupTime +} + +func getStartTime(startupTime *time.Time, readBuildInfo func() (info *debug.BuildInfo, ok bool)) func() { + return func() { + info, ok := readBuildInfo() if !ok { - startupTime = time.Now() + *startupTime = time.Now() return } @@ -38,7 +44,7 @@ func GetVersionTimestamp() time.Time { // Parse the build timestamp buildTime, err := time.Parse(time.RFC3339, setting.Value) if err == nil { - startupTime = buildTime + *startupTime = buildTime return } break @@ -46,10 +52,8 @@ func GetVersionTimestamp() time.Time { } // Return current time if no build time found - startupTime = time.Now() - }) - - return startupTime + *startupTime = time.Now() + } } func GetVersionUnixTimestamp() int64 { diff --git a/internal/serviceconfig/build/build_test.go b/internal/serviceconfig/build/build_test.go index 86430108..399293d0 100644 --- a/internal/serviceconfig/build/build_test.go +++ b/internal/serviceconfig/build/build_test.go @@ -1,46 +1,45 @@ -package build_test +package build import ( + "runtime/debug" "testing" "time" - "queryorchestration/internal/serviceconfig/build" - "github.com/stretchr/testify/assert" ) func TestGetVersionUnixTimestamp(t *testing.T) { - t1 := build.GetVersionUnixTimestamp() + t1 := GetVersionUnixTimestamp() assert.NotZero(t, t1) time.Sleep(time.Millisecond * 100) - t2 := build.GetVersionUnixTimestamp() + t2 := GetVersionUnixTimestamp() assert.Equal(t, t1, t2) } func TestIsValidUnixVersion(t *testing.T) { - assert.NotNil(t, build.IsValidUnixVersion(0)) - assert.NotNil(t, build.IsValidUnixVersion(time.Now().Add(time.Second).UnixMilli())) - assert.Nil(t, build.IsValidUnixVersion(1)) + assert.NotNil(t, IsValidUnixVersion(0)) + assert.NotNil(t, IsValidUnixVersion(time.Now().Add(time.Second).UnixMilli())) + assert.Nil(t, IsValidUnixVersion(1)) } func TestGetVersionTimestamp(t *testing.T) { // Test 1: First call should set the timestamp - t1 := build.GetVersionTimestamp() + t1 := GetVersionTimestamp() if t1.IsZero() { t.Error("GetVersionTimestamp returned zero time") } // Test 2: Second call should return exactly the same time time.Sleep(time.Millisecond * 100) // Small delay to ensure time would be different if not cached - t2 := build.GetVersionTimestamp() + t2 := GetVersionTimestamp() if !t1.Equal(t2) { t.Errorf("Timestamps should be identical but got t1=%v, t2=%v", t1, t2) } // Test 3: Even after a longer delay, should still return the same time time.Sleep(time.Second) // Longer delay - t3 := build.GetVersionTimestamp() + t3 := GetVersionTimestamp() if !t1.Equal(t3) { t.Errorf("Timestamp changed after delay: original=%v, new=%v", t1, t3) } @@ -51,9 +50,46 @@ func TestGetVersionTimestamp(t *testing.T) { } } +func TestGetStartTime(t *testing.T) { + t.Run("vcs time", func(t *testing.T) { + var tt time.Time + expectedTime := time.Now() + f := getStartTime(&tt, func() (info *debug.BuildInfo, ok bool) { + return &debug.BuildInfo{ + Settings: []debug.BuildSetting{ + { + Key: "vcs.time", + Value: expectedTime.Format(time.RFC3339), + }, + }, + }, true + }) + assert.NotNil(t, f) + f() + assert.Equal(t, expectedTime.UTC().Truncate(time.Second), tt) + }) + t.Run("vcs time - invalid format", func(t *testing.T) { + var tt time.Time + expectedTime := time.Now() + f := getStartTime(&tt, func() (info *debug.BuildInfo, ok bool) { + return &debug.BuildInfo{ + Settings: []debug.BuildSetting{ + { + Key: "vcs.time", + Value: "invalid", + }, + }, + }, true + }) + assert.NotNil(t, f) + f() + assert.True(t, expectedTime.Before(tt)) + }) +} + func TestGetVersion(t *testing.T) { // Test 1: Basic call - version := build.GetVersion() + version := GetVersion() if version == "" { t.Error("Version should not be empty") } @@ -67,7 +103,7 @@ func TestGetVersion(t *testing.T) { } // Test 3: Multiple calls should be consistent - version2 := build.GetVersion() + version2 := GetVersion() if version != version2 { t.Errorf("Version not consistent between calls: '%s' != '%s'", version, version2) } diff --git a/internal/test/container_test.go b/internal/test/container_test.go index ebb669f3..b4678eaf 100644 --- a/internal/test/container_test.go +++ b/internal/test/container_test.go @@ -6,6 +6,7 @@ import ( "queryorchestration/internal/serviceconfig" + "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" ) @@ -31,6 +32,12 @@ func TestCreateContainer(t *testing.T) { Name: QueryService, Cfg: cfg, Network: ncfg, + Env: map[string]string{ + "EXAMPLE": "value", + }, + ExposedPorts: []nat.Port{ + nat.Port("8080/tcp"), + }, } container, cleanup := createContainer(t, ctx, ccfg) diff --git a/serviceAPIs/queryService.yaml b/serviceAPIs/queryService.yaml index 727858fa..a6293a20 100644 --- a/serviceAPIs/queryService.yaml +++ b/serviceAPIs/queryService.yaml @@ -60,6 +60,8 @@ paths: - ClientService summary: Get a client by ID description: Retrieves a specific client by its ID. + security: + - placeholderAuth: [] responses: "200": description: Client details.