From 9481b2fb14dbe355f0b170f937a331ead98f5977 Mon Sep 17 00:00:00 2001 From: Shari Vietry Date: Mon, 23 Feb 2026 09:23:14 -0500 Subject: [PATCH] test: improve test coverage Signed-off-by: Shari Vietry --- internal/logger/logger_test.go | 57 +++++++++++++ internal/tracker/time_test.go | 21 +++++ pkg/connection/connection_test.go | 73 +++++++++++++++++ pkg/inventory/ecs_test.go | 89 +++++++++++++++++++++ pkg/inventory/report_test.go | 117 +++++++++++++++++++++++++++ pkg/lib_test.go | 24 ++++++ pkg/reporter/reporter_test.go | 129 ++++++++++++++++++++++++++++++ 7 files changed, 510 insertions(+) create mode 100644 internal/tracker/time_test.go create mode 100644 pkg/connection/connection_test.go create mode 100644 pkg/lib_test.go diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 32bfafd..1a76a32 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -39,3 +39,60 @@ func TestLoggerDefaultsToInfoLevelOnInvalidLevel(t *testing.T) { assert.Equal(t, zapLogger.zap.Level().String(), "info") } + +func TestNoOpLoggerMethods(t *testing.T) { + noop := &NoOpLogger{} + + // Verify none of the methods panic + t.Run("Debug", func(t *testing.T) { + noop.Debug("test message", "key", "value") + }) + t.Run("Debugf", func(t *testing.T) { + noop.Debugf("test %s", "message") + }) + t.Run("Info", func(t *testing.T) { + noop.Info("test message", "key", "value") + }) + t.Run("Warn", func(t *testing.T) { + noop.Warn("test message", "key", "value") + }) + t.Run("Warnf", func(t *testing.T) { + noop.Warnf("test %s", "message") + }) + t.Run("Error", func(t *testing.T) { + noop.Error("test message", assert.AnError, "key", "value") + }) +} + +func TestZapLoggerMethods(t *testing.T) { + tmpDir := t.TempDir() + fileLocation := path.Join(tmpDir, "test.log") + + zapLogger := InitZapLogger(LogConfig{Level: "debug", FileLocation: fileLocation}) + + t.Run("Debug", func(t *testing.T) { + zapLogger.Debug("debug message", "key", "value") + }) + t.Run("Debugf", func(t *testing.T) { + zapLogger.Debugf("debugf %s", "message") + }) + t.Run("Info", func(t *testing.T) { + zapLogger.Info("info message", "key", "value") + }) + t.Run("Warn", func(t *testing.T) { + zapLogger.Warn("warn message", "key", "value") + }) + t.Run("Warnf", func(t *testing.T) { + zapLogger.Warnf("warnf %s", "message") + }) + t.Run("Error", func(t *testing.T) { + zapLogger.Error("error message", assert.AnError, "key", "value") + }) + + // Verify log file was written to + b, err := os.ReadFile(fileLocation) + assert.NoError(t, err) + assert.Contains(t, string(b), "debug message") + assert.Contains(t, string(b), "warn message") + assert.Contains(t, string(b), "error message") +} diff --git a/internal/tracker/time_test.go b/internal/tracker/time_test.go new file mode 100644 index 0000000..89c9484 --- /dev/null +++ b/internal/tracker/time_test.go @@ -0,0 +1,21 @@ +package tracker + +import ( + "testing" + "time" + + "github.com/anchore/ecs-inventory/internal/logger" +) + +func TestTrackFunctionTime(t *testing.T) { + // Initialize the logger so the function can log without panicking + logger.Log = logger.InitZapLogger(logger.LogConfig{Level: "debug", FileLocation: ""}) + + t.Run("does not panic with current time", func(t *testing.T) { + TrackFunctionTime(time.Now(), "test function") + }) + + t.Run("does not panic with past time", func(t *testing.T) { + TrackFunctionTime(time.Now().Add(-5*time.Second), "past time test") + }) +} diff --git a/pkg/connection/connection_test.go b/pkg/connection/connection_test.go new file mode 100644 index 0000000..1ccb4e2 --- /dev/null +++ b/pkg/connection/connection_test.go @@ -0,0 +1,73 @@ +package connection + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAnchoreInfo_IsValid(t *testing.T) { + tests := []struct { + name string + info AnchoreInfo + want bool + }{ + { + name: "all fields populated", + info: AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "foobar", + Account: "test", + }, + want: true, + }, + { + name: "empty URL", + info: AnchoreInfo{ + URL: "", + User: "admin", + Password: "foobar", + }, + want: false, + }, + { + name: "empty User", + info: AnchoreInfo{ + URL: "https://ancho.re", + User: "", + Password: "foobar", + }, + want: false, + }, + { + name: "empty Password", + info: AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "", + }, + want: false, + }, + { + name: "all empty", + info: AnchoreInfo{}, + want: false, + }, + { + name: "Account empty but URL User Password set", + info: AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "foobar", + Account: "", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.info.IsValid()) + }) + } +} diff --git a/pkg/inventory/ecs_test.go b/pkg/inventory/ecs_test.go index 5a73d35..09d4711 100644 --- a/pkg/inventory/ecs_test.go +++ b/pkg/inventory/ecs_test.go @@ -496,6 +496,95 @@ func Test_constructServiceARN(t *testing.T) { } } +func Test_buildContainerTagMap(t *testing.T) { + tests := []struct { + name string + tasks []ecstypes.Task + want map[string]string + }{ + { + name: "empty task list", + tasks: []ecstypes.Task{}, + want: map[string]string{}, + }, + { + name: "containers with @ in image are excluded", + tasks: []ecstypes.Task{ + { + Containers: []ecstypes.Container{ + { + Image: aws.String("image-1@sha256:abc123"), + ImageDigest: aws.String("sha256:abc123"), + }, + }, + }, + }, + want: map[string]string{}, + }, + { + name: "containers with clean image tags are included", + tasks: []ecstypes.Task{ + { + Containers: []ecstypes.Container{ + { + Image: aws.String("nginx:latest"), + ImageDigest: aws.String("sha256:abc123"), + }, + { + Image: aws.String("redis:7.0"), + ImageDigest: aws.String("sha256:def456"), + }, + }, + }, + }, + want: map[string]string{ + "sha256:abc123": "nginx:latest", + "sha256:def456": "redis:7.0", + }, + }, + { + name: "mix of clean and @ images", + tasks: []ecstypes.Task{ + { + Containers: []ecstypes.Container{ + { + Image: aws.String("nginx:latest"), + ImageDigest: aws.String("sha256:abc123"), + }, + { + Image: aws.String("redis@sha256:def456"), + ImageDigest: aws.String("sha256:def456"), + }, + }, + }, + }, + want: map[string]string{ + "sha256:abc123": "nginx:latest", + }, + }, + { + name: "nil image digest is skipped", + tasks: []ecstypes.Task{ + { + Containers: []ecstypes.Container{ + { + Image: aws.String("nginx:latest"), + ImageDigest: nil, + }, + }, + }, + }, + want: map[string]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildContainerTagMap(tt.tasks) + assert.Equal(t, tt.want, got) + }) + } +} + func Test_getContainerImageTag(t *testing.T) { type args struct { containerTagMap map[string]string diff --git a/pkg/inventory/report_test.go b/pkg/inventory/report_test.go index 8f3c86f..9bdceca 100644 --- a/pkg/inventory/report_test.go +++ b/pkg/inventory/report_test.go @@ -1,14 +1,25 @@ package inventory import ( + "bytes" "context" + "encoding/json" + "os" "testing" + "github.com/h2non/gock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/anchore/ecs-inventory/internal/logger" + "github.com/anchore/ecs-inventory/pkg/connection" "github.com/anchore/ecs-inventory/pkg/reporter" ) +func init() { + logger.Log = &logger.NoOpLogger{} +} + func TestGetInventoryReportForCluster(t *testing.T) { mockSvc := &mockECSClient{} @@ -18,6 +29,112 @@ func TestGetInventoryReportForCluster(t *testing.T) { assert.Equal(t, 4, len(report.Containers)) } +func TestHandleReport(t *testing.T) { + testReport := reporter.Report{ + Timestamp: "2024-01-01T00:00:00Z", + ClusterARN: "arn:aws:ecs:us-east-1:123456789012:cluster/test", + Containers: []reporter.Container{ + { + ARN: "arn:aws:ecs:us-east-1:123456789012:container/abc", + ImageTag: "nginx:latest", + ImageDigest: "sha256:abc123", + TaskARN: "arn:aws:ecs:us-east-1:123456789012:task/test/task1", + }, + }, + } + + validAnchore := connection.AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "foobar", + Account: "test", + HTTP: connection.HTTPConfig{ + TimeoutSeconds: 10, + Insecure: true, + }, + } + + invalidAnchore := connection.AnchoreInfo{} + + t.Run("dry run does not post or print", func(t *testing.T) { + err := HandleReport(testReport, validAnchore, true, true) + assert.NoError(t, err) + }) + + t.Run("valid anchore quiet posts to anchore", func(t *testing.T) { + defer gock.Off() + gock.New("https://ancho.re"). + Post("v2/ecs-inventory"). + Reply(201). + JSON(map[string]interface{}{}) + + err := HandleReport(testReport, validAnchore, true, false) + assert.NoError(t, err) + assert.True(t, gock.IsDone()) + }) + + t.Run("invalid anchore not quiet prints to stdout", func(t *testing.T) { + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := HandleReport(testReport, invalidAnchore, false, false) + + w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + assert.NoError(t, err) + assert.Contains(t, output, testReport.ClusterARN) + }) + + t.Run("invalid anchore quiet does not print", func(t *testing.T) { + err := HandleReport(testReport, invalidAnchore, true, false) + assert.NoError(t, err) + }) +} + +func Test_reportToStdout(t *testing.T) { + testReport := reporter.Report{ + Timestamp: "2024-01-01T00:00:00Z", + ClusterARN: "arn:aws:ecs:us-east-1:123456789012:cluster/test", + Containers: []reporter.Container{ + { + ARN: "arn:aws:ecs:us-east-1:123456789012:container/abc", + ImageTag: "nginx:latest", + ImageDigest: "sha256:abc123", + TaskARN: "arn:aws:ecs:us-east-1:123456789012:task/test/task1", + }, + }, + } + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := reportToStdout(testReport) + + w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + require.NoError(t, err) + + var decoded reporter.Report + err = json.Unmarshal([]byte(output), &decoded) + require.NoError(t, err) + assert.Equal(t, testReport.ClusterARN, decoded.ClusterARN) + assert.Equal(t, testReport.Timestamp, decoded.Timestamp) + assert.Len(t, decoded.Containers, 1) + assert.Equal(t, "nginx:latest", decoded.Containers[0].ImageTag) +} + func Test_ensureReferencedObjectsExist(t *testing.T) { type args struct { report reporter.Report diff --git a/pkg/lib_test.go b/pkg/lib_test.go new file mode 100644 index 0000000..368ba6d --- /dev/null +++ b/pkg/lib_test.go @@ -0,0 +1,24 @@ +package pkg + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/ecs-inventory/pkg/logger" +) + +type mockLogger struct{} + +func (m *mockLogger) Error(msg string, err error, args ...interface{}) {} +func (m *mockLogger) Warn(msg string, args ...interface{}) {} +func (m *mockLogger) Warnf(msg string, args ...interface{}) {} +func (m *mockLogger) Info(msg string, args ...interface{}) {} +func (m *mockLogger) Debug(msg string, args ...interface{}) {} +func (m *mockLogger) Debugf(msg string, args ...interface{}) {} + +func TestSetLogger(t *testing.T) { + mock := &mockLogger{} + SetLogger(mock) + assert.Equal(t, logger.Logger(mock), log) +} diff --git a/pkg/reporter/reporter_test.go b/pkg/reporter/reporter_test.go index c7ebc61..54cc91a 100644 --- a/pkg/reporter/reporter_test.go +++ b/pkg/reporter/reporter_test.go @@ -1,10 +1,12 @@ package reporter import ( + "io" "testing" "github.com/h2non/gock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/anchore/ecs-inventory/pkg/connection" ) @@ -207,3 +209,130 @@ func TestPostSimulateV1ToV2HandoverFromEnterprise4Xto5X(t *testing.T) { assert.NoError(t, err) assert.Equal(t, v2ReportAPIPath, apiPath) } + +func Test_prepareRequest(t *testing.T) { + // Reset apiPath to default + apiPath = v2ReportAPIPath + + report := Report{ + Timestamp: "2024-01-01T00:00:00Z", + ClusterARN: "arn:aws:ecs:us-east-1:123456789012:cluster/test", + Containers: []Container{ + { + ARN: "arn:aws:ecs:us-east-1:123456789012:container/abc", + ImageTag: "nginx:latest", + ImageDigest: "sha256:abc123", + TaskARN: "arn:aws:ecs:us-east-1:123456789012:task/test/task1", + }, + }, + } + + anchoreDetails := connection.AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "foobar", + Account: "testaccount", + } + + req, err := prepareRequest(report, anchoreDetails) + require.NoError(t, err) + + // Verify URL + assert.Equal(t, "https://ancho.re/v2/ecs-inventory", req.URL.String()) + + // Verify method + assert.Equal(t, "POST", req.Method) + + // Verify basic auth + user, pass, ok := req.BasicAuth() + assert.True(t, ok) + assert.Equal(t, "admin", user) + assert.Equal(t, "foobar", pass) + + // Verify Content-Type + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + + // Verify x-anchore-account header + assert.Equal(t, "testaccount", req.Header.Get("x-anchore-account")) + + // Verify body contains report data + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "arn:aws:ecs:us-east-1:123456789012:cluster/test") + assert.Contains(t, string(body), "nginx:latest") +} + +func Test_fetchVersionedAPIPath(t *testing.T) { + t.Run("returns v2 path when API version is 2", func(t *testing.T) { + defer gock.Off() + gock.New("https://ancho.re"). + Get("/version"). + Reply(200). + JSON(map[string]interface{}{ + "api": map[string]interface{}{"version": "2"}, + "db": map[string]interface{}{"schema_version": "400"}, + "service": map[string]interface{}{"version": "5.0.0"}, + }) + + anchoreDetails := connection.AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "foobar", + HTTP: connection.HTTPConfig{ + TimeoutSeconds: 10, + Insecure: true, + }, + } + + path, err := fetchVersionedAPIPath(anchoreDetails) + assert.NoError(t, err) + assert.Equal(t, v2ReportAPIPath, path) + }) + + t.Run("returns v1 path when API version is not 2", func(t *testing.T) { + defer gock.Off() + gock.New("https://ancho.re"). + Get("/version"). + Reply(200). + JSON(map[string]interface{}{ + "api": map[string]interface{}{}, + "db": map[string]interface{}{"schema_version": "400"}, + "service": map[string]interface{}{"version": "4.8.0"}, + }) + + anchoreDetails := connection.AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "foobar", + HTTP: connection.HTTPConfig{ + TimeoutSeconds: 10, + Insecure: true, + }, + } + + path, err := fetchVersionedAPIPath(anchoreDetails) + assert.NoError(t, err) + assert.Equal(t, v1ReportAPIPath, path) + }) + + t.Run("returns v1 path on non-200 response", func(t *testing.T) { + defer gock.Off() + gock.New("https://ancho.re"). + Get("/version"). + Reply(500) + + anchoreDetails := connection.AnchoreInfo{ + URL: "https://ancho.re", + User: "admin", + Password: "foobar", + HTTP: connection.HTTPConfig{ + TimeoutSeconds: 10, + Insecure: true, + }, + } + + path, err := fetchVersionedAPIPath(anchoreDetails) + assert.Error(t, err) + assert.Equal(t, v1ReportAPIPath, path) + }) +}