diff --git a/.golangci.yml b/.golangci.yml index ef8a6f6d3..3ceba6b8c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,6 +3,7 @@ linters: default: all disable: - depguard + - paralleltest - tagliatelle - testpackage settings: diff --git a/internal/os/api/exit_code_test.go b/internal/os/api/exit_code_test.go new file mode 100644 index 000000000..bd3200590 --- /dev/null +++ b/internal/os/api/exit_code_test.go @@ -0,0 +1,129 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExitCodeErr_Error(t *testing.T) { + tests := []struct { + name string + exitCode int + expected string + }{ + { + name: "exit code 0", + exitCode: 0, + expected: "returned non zero exit: 0", + }, + { + name: "exit code 1", + exitCode: 1, + expected: "returned non zero exit: 1", + }, + { + name: "exit code 3 (restart)", + exitCode: 3, + expected: "returned non zero exit: 3", + }, + { + name: "negative exit code", + exitCode: -1, + expected: "returned non zero exit: -1", + }, + { + name: "large exit code", + exitCode: 255, + expected: "returned non zero exit: 255", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := &ExitCodeErr{exitCode: tt.exitCode} + assert.Equal(t, tt.expected, err.Error()) + }) + } +} + +func TestExitCodeErr_ExitCode(t *testing.T) { + tests := []struct { + name string + exitCode int + }{ + { + name: "exit code success", + exitCode: ExitCodeSuccess, + }, + { + name: "exit code error", + exitCode: ExitCodeError, + }, + { + name: "exit code restart", + exitCode: ExitCodeRestart, + }, + { + name: "custom exit code", + exitCode: 42, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := &ExitCodeErr{exitCode: tt.exitCode} + assert.Equal(t, tt.exitCode, err.ExitCode()) + }) + } +} + +func TestNewExitCodeErr(t *testing.T) { + tests := []struct { + name string + exitCode int + }{ + { + name: "create with success code", + exitCode: ExitCodeSuccess, + }, + { + name: "create with error code", + exitCode: ExitCodeError, + }, + { + name: "create with restart code", + exitCode: ExitCodeRestart, + }, + { + name: "create with negative code", + exitCode: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := NewExitCodeErr(tt.exitCode) + assert.NotNil(t, err) + assert.Equal(t, tt.exitCode, err.ExitCode()) + }) + } +} + +func TestExitCodeConstants(t *testing.T) { + assert.Equal(t, 0, ExitCodeSuccess) + assert.Equal(t, 1, ExitCodeError) + assert.Equal(t, 3, ExitCodeRestart) +} + +func TestCheckExitCode_NilError(t *testing.T) { + result := CheckExitCode(nil) + assert.Equal(t, ExitCodeSuccess, result) +} + +func TestExitCodeErr_ImplementsError(_ *testing.T) { + var _ error = (*ExitCodeErr)(nil) +} diff --git a/internal/os/fs/fs_test.go b/internal/os/fs/fs_test.go new file mode 100644 index 000000000..3b53414ff --- /dev/null +++ b/internal/os/fs/fs_test.go @@ -0,0 +1,217 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package fs + +import ( + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadFirstLine(t *testing.T) { + tests := []struct { + name string + content string + expected string + expectError bool + }{ + { + name: "single line", + content: "Hello World", + expected: "Hello World", + expectError: false, + }, + { + name: "multiple lines returns first", + content: "First Line\nSecond Line\nThird Line", + expected: "First Line", + expectError: false, + }, + { + name: "trims whitespace", + content: " Trimmed Content \nSecond Line", + expected: "Trimmed Content", + expectError: false, + }, + { + name: "empty file", + content: "", + expected: "", + expectError: false, + }, + { + name: "only newline", + content: "\n", + expected: "", + expectError: false, + }, + { + name: "tabs and spaces", + content: "\t Hello \t\nWorld", + expected: "Hello", + expectError: false, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + tmpFile := createTempFile(t, tt.content) + defer os.Remove(tmpFile) + + result, err := ReadFirstLine(tmpFile) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestReadFirstLine_FileNotFound(t *testing.T) { + result, err := ReadFirstLine("/nonexistent/path/file.txt") + require.Error(t, err) + assert.Equal(t, "unknown", result) + assert.Contains(t, err.Error(), "cannot open file") +} + +func TestReadFileFieldMatching(t *testing.T) { + tests := []struct { + name string + content string + pattern string + expected string + expectError bool + }{ + { + name: "match on first line", + content: "VERSION=1.2.3\nNAME=test", + pattern: `VERSION=(.+)`, + expected: "1.2.3", + expectError: false, + }, + { + name: "match on second line", + content: "NAME=test\nVERSION=1.2.3", + pattern: `VERSION=(.+)`, + expected: "1.2.3", + expectError: false, + }, + { + name: "match with quotes", + content: `NAME="Ubuntu"\nVERSION="20.04"`, + pattern: `NAME="([^"]+)"`, + expected: "Ubuntu", + expectError: false, + }, + { + name: "no match returns unknown", + content: "FOO=bar\nBAZ=qux", + pattern: `VERSION=(.+)`, + expected: "unknown", + expectError: false, + }, + { + name: "complex regex", + content: "Red Hat Enterprise Linux release 8.4 (Ootpa)", + pattern: `release (\d+\.\d+)`, + expected: "8.4", + expectError: false, + }, + { + name: "empty file returns unknown", + content: "", + pattern: `(.+)`, + expected: "unknown", + expectError: false, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + tmpFile := createTempFile(t, tt.content) + + defer os.Remove(tmpFile) + + re := regexp.MustCompile(tt.pattern) + + result, err := ReadFileFieldMatching(tmpFile, re) + if tt.expectError { + assert.Error(t, err) + } else { + // Note: err may still be nil even when result is "unknown" + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestReadFileFieldMatching_FileNotFound(t *testing.T) { + re := regexp.MustCompile(`(.+)`) + result, err := ReadFileFieldMatching("/nonexistent/path/file.txt", re) + require.Error(t, err) + assert.Equal(t, "unknown", result) + assert.Contains(t, err.Error(), "cannot open file") +} + +func TestReadFileFieldMatching_MultipleCaptures(t *testing.T) { + content := "VERSION_ID=8.4" + + tmpFile := createTempFile(t, content) + defer os.Remove(tmpFile) + + // Pattern with multiple capture groups - should return first capture + re := regexp.MustCompile(`VERSION_ID=(\d+)\.(\d+)`) + result, err := ReadFileFieldMatching(tmpFile, re) + require.NoError(t, err) + assert.Equal(t, "8", result) // First capture group +} + +func TestReadFileFieldMatching_LinuxReleaseFiles(t *testing.T) { + t.Run("os-release NAME", func(t *testing.T) { + tmpFile := createTempFile(t, `NAME="Ubuntu"\nVERSION="20.04.3 LTS (Focal Fossa)"`) + defer os.Remove(tmpFile) + + re := regexp.MustCompile(`NAME="?([^"\n]+)"?`) + result, err := ReadFileFieldMatching(tmpFile, re) + require.NoError(t, err) + assert.Equal(t, "Ubuntu", result) + }) + + t.Run("centos-release", func(t *testing.T) { + tmpFile := createTempFile(t, "CentOS Linux release 7.9.2009 (Core)") + defer os.Remove(tmpFile) + + re := regexp.MustCompile(`release (\d+\.\d+)`) + result, err := ReadFileFieldMatching(tmpFile, re) + require.NoError(t, err) + assert.Equal(t, "7.9", result) + }) + + t.Run("redhat-release", func(t *testing.T) { + tmpFile := createTempFile(t, "Red Hat Enterprise Linux release 8.4 (Ootpa)") + defer os.Remove(tmpFile) + + re := regexp.MustCompile(`release (\d+\.\d+)`) + result, err := ReadFileFieldMatching(tmpFile, re) + require.NoError(t, err) + assert.Equal(t, "8.4", result) + }) +} + +// createTempFile creates a temporary file with the given content and returns its path. +func createTempFile(t *testing.T, content string) string { + t.Helper() + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "testfile") + err := os.WriteFile(tmpFile, []byte(content), 0o600) + require.NoError(t, err) + + return tmpFile +} diff --git a/internal/plugins/linux/cloud_security_groups_test.go b/internal/plugins/linux/cloud_security_groups_test.go new file mode 100644 index 000000000..204b17c04 --- /dev/null +++ b/internal/plugins/linux/cloud_security_groups_test.go @@ -0,0 +1,62 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//go:build linux || darwin + +package linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCloudSecurityGroup_SortKey(t *testing.T) { + tests := []struct { + name string + group CloudSecurityGroup + expected string + }{ + { + name: "standard security group", + group: CloudSecurityGroup{ + SecurityGroup: "sg-12345678", + }, + expected: "sg-12345678", + }, + { + name: "security group with name", + group: CloudSecurityGroup{ + SecurityGroup: "default", + }, + expected: "default", + }, + { + name: "empty security group", + group: CloudSecurityGroup{ + SecurityGroup: "", + }, + expected: "", + }, + { + name: "security group with special characters", + group: CloudSecurityGroup{ + SecurityGroup: "my-security-group_v1", + }, + expected: "my-security-group_v1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.group.SortKey()) + }) + } +} + +func TestCloudSecurityGroup_Fields(t *testing.T) { + group := CloudSecurityGroup{ + SecurityGroup: "sg-abcdef123", + } + + assert.Equal(t, "sg-abcdef123", group.SecurityGroup) +} diff --git a/internal/plugins/linux/dpkg_test.go b/internal/plugins/linux/dpkg_test.go new file mode 100644 index 000000000..6496536c6 --- /dev/null +++ b/internal/plugins/linux/dpkg_test.go @@ -0,0 +1,74 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//go:build linux + +package linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDpkgItem_SortKey(t *testing.T) { + tests := []struct { + name string + item DpkgItem + expected string + }{ + { + name: "standard package name", + item: DpkgItem{ + Name: "nginx", + Architecture: "amd64", + Version: "1.18.0", + }, + expected: "nginx", + }, + { + name: "package with special characters", + item: DpkgItem{ + Name: "libc6-dev", + }, + expected: "libc6-dev", + }, + { + name: "empty name", + item: DpkgItem{ + Name: "", + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.item.SortKey()) + }) + } +} + +func TestDpkgItem_Fields(t *testing.T) { + item := DpkgItem{ + Name: "test-package", + Architecture: "amd64", + Essential: "no", + Priority: "optional", + Status: "installed", + Version: "1.0.0-1ubuntu1", + InstallTime: "1609459200", + } + + assert.Equal(t, "test-package", item.Name) + assert.Equal(t, "amd64", item.Architecture) + assert.Equal(t, "no", item.Essential) + assert.Equal(t, "optional", item.Priority) + assert.Equal(t, "installed", item.Status) + assert.Equal(t, "1.0.0-1ubuntu1", item.Version) + assert.Equal(t, "1609459200", item.InstallTime) +} + +func TestDpkgConstants(t *testing.T) { + assert.Equal(t, "/var/lib/dpkg/info", DPKG_INFO_DIR) + assert.Equal(t, 16, FSN_CLOSE_WRITE) +} diff --git a/internal/plugins/linux/error_test.go b/internal/plugins/linux/error_test.go new file mode 100644 index 000000000..3b51b1f38 --- /dev/null +++ b/internal/plugins/linux/error_test.go @@ -0,0 +1,20 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPluginDisabledErr(t *testing.T) { + require.Error(t, PluginDisabledErr) + assert.Equal(t, "frequency disabled plugin", PluginDisabledErr.Error()) +} + +func TestPluginDisabledErr_ImplementsError(_ *testing.T) { + _ = PluginDisabledErr +} diff --git a/internal/plugins/linux/kernel_modules_test.go b/internal/plugins/linux/kernel_modules_test.go new file mode 100644 index 000000000..06e166b3a --- /dev/null +++ b/internal/plugins/linux/kernel_modules_test.go @@ -0,0 +1,139 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//go:build linux || darwin + +package linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKernelModule_SortKey(t *testing.T) { + t.Run("standard module name", func(t *testing.T) { + module := KernelModule{ + Name: "ext4", + Version: "1.0", + Description: "Fourth Extended Filesystem", + } + assert.Equal(t, "ext4", module.SortKey()) + }) + + t.Run("empty name", func(t *testing.T) { + module := KernelModule{ + Name: "", + Version: "", + Description: "", + } + assert.Empty(t, module.SortKey()) + }) + + t.Run("module with special characters", func(t *testing.T) { + module := KernelModule{ + Name: "nf_conntrack_ipv4", + Version: "", + Description: "", + } + assert.Equal(t, "nf_conntrack_ipv4", module.SortKey()) + }) +} + +func TestKernelModule_Fields(t *testing.T) { + module := KernelModule{ + Name: "test_module", + Version: "2.0.1", + Description: "Test module description", + } + + assert.Equal(t, "test_module", module.Name) + assert.Equal(t, "2.0.1", module.Version) + assert.Equal(t, "Test module description", module.Description) +} + +func TestKernelModulesPlugin_getKernelModulesDataset(t *testing.T) { + t.Run("empty modules", func(t *testing.T) { + plugin := &KernelModulesPlugin{ //nolint:exhaustruct + loadedModules: make(map[string]KernelModule), + } + dataset := plugin.getKernelModulesDataset() + assert.Empty(t, dataset) + }) + + t.Run("single module", func(t *testing.T) { + plugin := &KernelModulesPlugin{ //nolint:exhaustruct + loadedModules: map[string]KernelModule{ + "ext4": {Name: "ext4", Version: "1.0", Description: ""}, + }, + } + dataset := plugin.getKernelModulesDataset() + assert.Len(t, dataset, 1) + }) + + t.Run("multiple modules", func(t *testing.T) { + plugin := &KernelModulesPlugin{ //nolint:exhaustruct + loadedModules: map[string]KernelModule{ + "ext4": {Name: "ext4", Version: "1.0", Description: ""}, + "xfs": {Name: "xfs", Version: "1.0", Description: ""}, + "btrfs": {Name: "btrfs", Version: "1.0", Description: ""}, + }, + } + dataset := plugin.getKernelModulesDataset() + assert.Len(t, dataset, 3) + }) +} + +func TestKernelModulesPlugin_processUpdates_Additions(t *testing.T) { + plugin := &KernelModulesPlugin{ //nolint:exhaustruct + loadedModules: make(map[string]KernelModule), + needsFlush: false, + } + + seenModules := map[string]bool{ + "newmodule": true, + } + + // Note: This will fail to get module info since modinfo is not available in test + // but it will still add the module to the map + _ = plugin.processUpdates(seenModules) + + // Module should be added to the map even if modinfo fails + assert.Contains(t, plugin.loadedModules, "newmodule") +} + +func TestKernelModulesPlugin_processUpdates_Removals(t *testing.T) { + plugin := &KernelModulesPlugin{ //nolint:exhaustruct + loadedModules: map[string]KernelModule{ + "oldmodule": {Name: "oldmodule", Version: "", Description: ""}, + }, + needsFlush: false, + } + + seenModules := map[string]bool{} // empty - module was removed + + err := plugin.processUpdates(seenModules) + require.NoError(t, err) + + assert.NotContains(t, plugin.loadedModules, "oldmodule") + assert.True(t, plugin.needsFlush) +} + +func TestKernelModulesPlugin_processUpdates_NoChanges(t *testing.T) { + plugin := &KernelModulesPlugin{ //nolint:exhaustruct + loadedModules: map[string]KernelModule{ + "existingmodule": {Name: "existingmodule", Version: "", Description: ""}, + }, + needsFlush: false, + } + + seenModules := map[string]bool{ + "existingmodule": true, + } + + err := plugin.processUpdates(seenModules) + require.NoError(t, err) + + assert.Contains(t, plugin.loadedModules, "existingmodule") + assert.False(t, plugin.needsFlush) // No changes, should remain false +} diff --git a/internal/plugins/linux/sysctl_test.go b/internal/plugins/linux/sysctl_test.go new file mode 100644 index 000000000..203439c74 --- /dev/null +++ b/internal/plugins/linux/sysctl_test.go @@ -0,0 +1,91 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//go:build linux || darwin + +package linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSysctlItem_SortKey(t *testing.T) { + tests := []struct { + name string + item SysctlItem + expected string + }{ + { + name: "standard sysctl", + item: SysctlItem{ + Sysctl: "net.ipv4.tcp_syncookies", + Value: "1", + }, + expected: "net.ipv4.tcp_syncookies", + }, + { + name: "kernel sysctl", + item: SysctlItem{ + Sysctl: "kernel.panic", + Value: "0", + }, + expected: "kernel.panic", + }, + { + name: "empty sysctl", + item: SysctlItem{ + Sysctl: "", + Value: "", + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.item.SortKey()) + }) + } +} + +func TestSysctlItem_Fields(t *testing.T) { + item := SysctlItem{ + Sysctl: "vm.swappiness", + Value: "60", + } + + assert.Equal(t, "vm.swappiness", item.Sysctl) + assert.Equal(t, "60", item.Value) +} + +func TestSysctlConstants(t *testing.T) { + assert.Equal(t, 0o222, WRITABLE_MASK) + assert.Equal(t, 0o444, READABLE_MASK) +} + +func TestIgnoredListPatterns(t *testing.T) { + // Verify that ignoredListPatterns is defined and has expected patterns + assert.NotEmpty(t, ignoredListPatterns) + assert.Greater(t, len(ignoredListPatterns), 5) + + // Check some specific patterns are present + foundKernelPattern := false + foundNetPattern := false + foundFsPattern := false + + for _, pattern := range ignoredListPatterns { + switch { + case len(pattern) >= 6 && pattern[:6] == "kernel": + foundKernelPattern = true + case len(pattern) >= 3 && pattern[:3] == "net": + foundNetPattern = true + case len(pattern) >= 2 && pattern[:2] == "fs": + foundFsPattern = true + } + } + + assert.True(t, foundKernelPattern, "Expected kernel pattern in ignoredListPatterns") + assert.True(t, foundNetPattern, "Expected net pattern in ignoredListPatterns") + assert.True(t, foundFsPattern, "Expected fs pattern in ignoredListPatterns") +} diff --git a/internal/plugins/linux/sysvinit_test.go b/internal/plugins/linux/sysvinit_test.go new file mode 100644 index 000000000..a90f47bb3 --- /dev/null +++ b/internal/plugins/linux/sysvinit_test.go @@ -0,0 +1,107 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//go:build linux + +package linux + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSysvService_SortKey(t *testing.T) { + tests := []struct { + name string + service SysvService + expected string + }{ + { + name: "standard service name", + service: SysvService{ + Name: "nginx", + }, + expected: "nginx", + }, + { + name: "service with hyphen", + service: SysvService{ + Name: "network-manager", + }, + expected: "network-manager", + }, + { + name: "empty name", + service: SysvService{ + Name: "", + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.service.SortKey()) + }) + } +} + +func TestSysvService_Fields(t *testing.T) { + service := SysvService{ + Name: "test-service", + } + + assert.Equal(t, "test-service", service.Name) +} + +func TestSysvInitDir(t *testing.T) { + assert.Equal(t, "/var/run", SYSV_INIT_DIR) +} + +func TestPidFileIsStale(t *testing.T) { + tests := []struct { + name string + pidFileMod time.Time + processStartTime time.Time + expected bool + }{ + { + name: "not stale - process started before pidfile", + pidFileMod: time.Now(), + processStartTime: time.Now().Add(-1 * time.Second), + expected: false, + }, + { + name: "not stale - same time", + pidFileMod: time.Now(), + processStartTime: time.Now(), + expected: false, + }, + { + name: "not stale - within 5 second threshold", + pidFileMod: time.Now().Add(-3 * time.Second), + processStartTime: time.Now(), + expected: false, + }, + { + name: "stale - process started more than 5 seconds after pidfile", + pidFileMod: time.Now().Add(-10 * time.Second), + processStartTime: time.Now(), + expected: true, + }, + { + name: "stale - large time difference", + pidFileMod: time.Now().Add(-1 * time.Hour), + processStartTime: time.Now(), + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := pidFileIsStale(tt.pidFileMod, tt.processStartTime) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/internal/plugins/linux/upstart_test.go b/internal/plugins/linux/upstart_test.go new file mode 100644 index 000000000..5336a39aa --- /dev/null +++ b/internal/plugins/linux/upstart_test.go @@ -0,0 +1,135 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//go:build linux || darwin + +package linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUpstartService_SortKey(t *testing.T) { + t.Run("standard service name", func(t *testing.T) { + service := UpstartService{ + Name: "nginx", + Pid: "1234", + } + assert.Equal(t, "nginx", service.SortKey()) + }) + + t.Run("service with special characters", func(t *testing.T) { + service := UpstartService{ + Name: "network-manager", + Pid: "5678", + } + assert.Equal(t, "network-manager", service.SortKey()) + }) + + t.Run("empty name", func(t *testing.T) { + service := UpstartService{ + Name: "", + Pid: "0", + } + assert.Empty(t, service.SortKey()) + }) +} + +func TestUpstartService_Fields(t *testing.T) { + service := UpstartService{ + Name: "test-service", + Pid: "12345", + } + + assert.Equal(t, "test-service", service.Name) + assert.Equal(t, "12345", service.Pid) +} + +func TestUpstartPlugin_getUpstartDataset(t *testing.T) { + t.Run("empty services", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: make(map[string]UpstartService), + } + dataset := plugin.getUpstartDataset() + assert.Empty(t, dataset) + }) + + t.Run("single service", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: map[string]UpstartService{ + "nginx": {Name: "nginx", Pid: "1234"}, + }, + } + dataset := plugin.getUpstartDataset() + assert.Len(t, dataset, 1) + }) + + t.Run("multiple services", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: map[string]UpstartService{ + "nginx": {Name: "nginx", Pid: "1234"}, + "mysql": {Name: "mysql", Pid: "5678"}, + "postfix": {Name: "postfix", Pid: "9012"}, + }, + } + dataset := plugin.getUpstartDataset() + assert.Len(t, dataset, 3) + }) +} + +func TestUpstartPlugin_getUpstartPidMap(t *testing.T) { + t.Run("empty services", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: make(map[string]UpstartService), + } + pidMap := plugin.getUpstartPidMap() + assert.Empty(t, pidMap) + }) + + t.Run("single service with valid pid", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: map[string]UpstartService{ + "nginx": {Name: "nginx", Pid: "1234"}, + }, + } + pidMap := plugin.getUpstartPidMap() + assert.Len(t, pidMap, 1) + assert.Equal(t, "nginx", pidMap[1234]) + }) + + t.Run("service with invalid pid", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: map[string]UpstartService{ + "nginx": {Name: "nginx", Pid: "unknown"}, + }, + } + pidMap := plugin.getUpstartPidMap() + assert.Empty(t, pidMap) + }) + + t.Run("multiple services", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: map[string]UpstartService{ + "nginx": {Name: "nginx", Pid: "1234"}, + "mysql": {Name: "mysql", Pid: "5678"}, + }, + } + pidMap := plugin.getUpstartPidMap() + assert.Len(t, pidMap, 2) + assert.Equal(t, "nginx", pidMap[1234]) + assert.Equal(t, "mysql", pidMap[5678]) + }) + + t.Run("mixed valid and invalid pids", func(t *testing.T) { + plugin := UpstartPlugin{ //nolint:exhaustruct + runningServices: map[string]UpstartService{ + "nginx": {Name: "nginx", Pid: "1234"}, + "unknown": {Name: "unknown", Pid: "invalid"}, + }, + } + pidMap := plugin.getUpstartPidMap() + assert.Len(t, pidMap, 1) + assert.Equal(t, "nginx", pidMap[1234]) + }) +} diff --git a/pkg/ctl/notification_handler_test.go b/pkg/ctl/notification_handler_test.go new file mode 100644 index 000000000..5502f68f2 --- /dev/null +++ b/pkg/ctl/notification_handler_test.go @@ -0,0 +1,179 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ctl + +import ( + "context" + "errors" + "testing" + + "github.com/newrelic/infrastructure-agent/pkg/ipc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var errListenerError = errors.New("listener error") + +func TestNewNotificationHandlerWithCancellation(t *testing.T) { + t.Run("with background context", func(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.Background()) + assert.NotNil(t, handler) + assert.NotNil(t, handler.ctx) + assert.NotNil(t, handler.cancel) + assert.NotNil(t, handler.handlers) + assert.NotNil(t, handler.listener) + }) + + t.Run("with nil context", func(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.TODO()) + assert.NotNil(t, handler) + assert.NotNil(t, handler.ctx) + assert.NotNil(t, handler.cancel) + assert.NotNil(t, handler.handlers) + assert.NotNil(t, handler.listener) + }) + + t.Run("with already cancelled context", func(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(cancelledContext()) + assert.NotNil(t, handler) + assert.NotNil(t, handler.ctx) + assert.NotNil(t, handler.cancel) + assert.NotNil(t, handler.handlers) + assert.NotNil(t, handler.listener) + }) +} + +func TestNotificationHandlerWithCancellation_RegisterHandler(t *testing.T) { + t.Run("register single handler", func(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.Background()) + handler.RegisterHandler(ipc.Message("TEST"), func() error { return nil }) + assert.Len(t, handler.handlers, 1) + }) + + t.Run("register multiple handlers", func(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.Background()) + handler.RegisterHandler(ipc.Message("TEST2"), func() error { return nil }) + assert.Len(t, handler.handlers, 1) + handler.RegisterHandler(ipc.Message("ANOTHER"), func() error { return nil }) + assert.Len(t, handler.handlers, 2) + }) +} + +func TestNotificationHandlerWithCancellation_Stop(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.Background()) + + // Verify context is not cancelled before stop + select { + case <-handler.ctx.Done(): + t.Fatal("context should not be cancelled before Stop()") + default: + } + + handler.Stop() + + // Verify context is cancelled after stop + select { + case <-handler.ctx.Done(): + default: + t.Fatal("context should be cancelled after Stop()") + } +} + +func TestNotificationHandlerWithCancellation_Start(t *testing.T) { + t.Run("listener returns no error", func(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.Background()) + handler.listener = func(_ context.Context, _ map[ipc.Message]func() error) error { + return nil + } + err := handler.Start() + require.NoError(t, err) + }) + + t.Run("listener returns error", func(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.Background()) + handler.listener = func(_ context.Context, _ map[ipc.Message]func() error) error { + return errListenerError //nolint:wrapcheck + } + err := handler.Start() + require.Error(t, err) + assert.Equal(t, errListenerError, err) + }) +} + +func TestNotificationHandlerWithCancellation_HandlerExecution(t *testing.T) { + executed := false + executedMessage := ipc.Message("") + + handler := NewNotificationHandlerWithCancellation(context.Background()) + + testHandler := func() error { + executed = true + + return nil + } + + handler.RegisterHandler(ipc.Message("TEST_MSG"), testHandler) + + // Replace listener with mock that executes handlers + handler.listener = func(_ context.Context, handlers map[ipc.Message]func() error) error { + for msg, h := range handlers { + executedMessage = msg + + return h() + } + + return nil + } + + err := handler.Start() + require.NoError(t, err) + + assert.True(t, executed) + assert.Equal(t, ipc.Message("TEST_MSG"), executedMessage) +} + +func TestNotificationHandlerWithCancellation_OverwriteHandler(t *testing.T) { + handler := NewNotificationHandlerWithCancellation(context.Background()) + + firstCalled := false + secondCalled := false + + // Register first handler + handler.RegisterHandler(ipc.Message("MSG"), func() error { + firstCalled = true + + return nil + }) + + // Register second handler with same message - should overwrite + handler.RegisterHandler(ipc.Message("MSG"), func() error { + secondCalled = true + + return nil + }) + + assert.Len(t, handler.handlers, 1) + + // Execute handler through mock listener + handler.listener = func(_ context.Context, handlers map[ipc.Message]func() error) error { + if h, ok := handlers[ipc.Message("MSG")]; ok { + return h() + } + + return nil + } + + err := handler.Start() + require.NoError(t, err) + + assert.False(t, firstCalled) + assert.True(t, secondCalled) +} + +func cancelledContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + return ctx +} diff --git a/pkg/disk/disk_unix_test.go b/pkg/disk/disk_unix_test.go new file mode 100644 index 000000000..aba6ca3ba --- /dev/null +++ b/pkg/disk/disk_unix_test.go @@ -0,0 +1,98 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//go:build linux || darwin + +package disk + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteFile(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "testfile.txt") + content := []byte("test content") + + err := WriteFile(testFile, content, 0o644) + require.NoError(t, err) + + // Verify file was created with correct content + readContent, err := os.ReadFile(testFile) + require.NoError(t, err) + assert.Equal(t, content, readContent) +} + +func TestOpenFile(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "openfile.txt") + + // Create file + f, err := OpenFile(testFile, os.O_CREATE|os.O_WRONLY, 0o644) + require.NoError(t, err) + assert.NotNil(t, f) + f.Close() + + // Verify file exists + _, err = os.Stat(testFile) + require.NoError(t, err) +} + +func TestCreate(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "created.txt") + + f, err := Create(testFile) + require.NoError(t, err) + assert.NotNil(t, f) + f.Close() + + // Verify file exists + _, err = os.Stat(testFile) + require.NoError(t, err) +} + +func TestMkdirAll(t *testing.T) { + tmpDir := t.TempDir() + nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3") + + err := MkdirAll(nestedDir, 0o755) + require.NoError(t, err) + + // Verify directory exists + info, err := os.Stat(nestedDir) + require.NoError(t, err) + assert.True(t, info.IsDir()) +} + +func TestWriteFile_EmptyContent(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "empty.txt") + + err := WriteFile(testFile, []byte{}, 0o644) + require.NoError(t, err) + + // Verify file was created + info, err := os.Stat(testFile) + require.NoError(t, err) + assert.Equal(t, int64(0), info.Size()) +} + +func TestOpenFile_ReadOnly(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "readonly.txt") + + // Create file first + err := WriteFile(testFile, []byte("test"), 0o644) + require.NoError(t, err) + + // Open for reading + f, err := OpenFile(testFile, os.O_RDONLY, 0) + require.NoError(t, err) + assert.NotNil(t, f) + f.Close() +} diff --git a/pkg/entity/host/host_test.go b/pkg/entity/host/host_test.go new file mode 100644 index 000000000..b0c4f7318 --- /dev/null +++ b/pkg/entity/host/host_test.go @@ -0,0 +1,203 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package host + +import ( + "testing" + + "github.com/newrelic/infrastructure-agent/pkg/sysinfo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIDLookup_AgentKey(t *testing.T) { + tests := []struct { + name string + lookup IDLookup + expectedKey string + expectError error + }{ + { + name: "empty lookup returns error", + lookup: IDLookup{}, + expectedKey: "", + expectError: ErrNoEntityKeys, + }, + { + name: "instance id takes priority", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_INSTANCE_ID: "i-12345", + sysinfo.HOST_SOURCE_HOSTNAME: "myhost", + }, + expectedKey: "i-12345", + expectError: nil, + }, + { + name: "hostname when no cloud id", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_HOSTNAME: "myhost.example.com", + }, + expectedKey: "myhost.example.com", + expectError: nil, + }, + { + name: "azure vm id", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_AZURE_VM_ID: "azure-vm-123", + sysinfo.HOST_SOURCE_HOSTNAME: "myhost", + }, + expectedKey: "azure-vm-123", + expectError: nil, + }, + { + name: "gcp vm id", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_GCP_VM_ID: "gcp-instance-456", + sysinfo.HOST_SOURCE_HOSTNAME: "myhost", + }, + expectedKey: "gcp-instance-456", + expectError: nil, + }, + { + name: "display name takes priority over hostname", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_DISPLAY_NAME: "My Display Name", + sysinfo.HOST_SOURCE_HOSTNAME: "myhost", + }, + expectedKey: "My Display Name", + expectError: nil, + }, + { + name: "skip empty values", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_INSTANCE_ID: "", + sysinfo.HOST_SOURCE_HOSTNAME: "myhost", + }, + expectedKey: "myhost", + expectError: nil, + }, + { + name: "undefined lookup type when only empty values", + lookup: IDLookup{ + "unknown_key": "value", + }, + expectedKey: "", + expectError: ErrUndefinedLookupType, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + key, err := tt.lookup.AgentKey() + if tt.expectError != nil { + require.Error(t, err) + assert.Equal(t, tt.expectError, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedKey, key) + } + }) + } +} + +func TestIDLookup_AgentShortEntityName(t *testing.T) { + tests := []struct { + name string + lookup IDLookup + expectedName string + expectError error + }{ + { + name: "empty lookup returns error", + lookup: IDLookup{}, + expectedName: "", + expectError: ErrUndefinedLookupType, + }, + { + name: "instance id takes priority", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_INSTANCE_ID: "i-12345", + sysinfo.HOST_SOURCE_HOSTNAME_SHORT: "myhost", + }, + expectedName: "i-12345", + expectError: nil, + }, + { + name: "short hostname when no cloud id", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_HOSTNAME_SHORT: "myhost", + }, + expectedName: "myhost", + expectError: nil, + }, + { + name: "display name takes priority over short hostname", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_DISPLAY_NAME: "My Display Name", + sysinfo.HOST_SOURCE_HOSTNAME_SHORT: "myhost", + }, + expectedName: "My Display Name", + expectError: nil, + }, + { + name: "skip empty values", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_INSTANCE_ID: "", + sysinfo.HOST_SOURCE_HOSTNAME_SHORT: "myhost", + }, + expectedName: "myhost", + expectError: nil, + }, + { + name: "alibaba vm id", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_ALIBABA_VM_ID: "alibaba-123", + sysinfo.HOST_SOURCE_HOSTNAME_SHORT: "myhost", + }, + expectedName: "alibaba-123", + expectError: nil, + }, + { + name: "oci vm id", + lookup: IDLookup{ + sysinfo.HOST_SOURCE_OCI_VM_ID: "oci-456", + sysinfo.HOST_SOURCE_HOSTNAME_SHORT: "myhost", + }, + expectedName: "oci-456", + expectError: nil, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + name, err := tt.lookup.AgentShortEntityName() + if tt.expectError != nil { + require.Error(t, err) + assert.Equal(t, tt.expectError, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedName, name) + } + }) + } +} + +func TestErrorVariables(t *testing.T) { + require.Error(t, ErrUndefinedLookupType) + require.Error(t, ErrNoEntityKeys) + + assert.Equal(t, "no known identifier types found in ID lookup table", ErrUndefinedLookupType.Error()) + assert.Equal(t, "no agent identifiers available", ErrNoEntityKeys.Error()) +} + +func TestIDLookup_IsMap(t *testing.T) { + lookup := IDLookup{ + "key1": "value1", + "key2": "value2", + } + + assert.Len(t, lookup, 2) + assert.Equal(t, "value1", lookup["key1"]) + assert.Equal(t, "value2", lookup["key2"]) +} diff --git a/pkg/helpers/fingerprint/fingerprint_extended_test.go b/pkg/helpers/fingerprint/fingerprint_extended_test.go new file mode 100644 index 000000000..5d51e22e3 --- /dev/null +++ b/pkg/helpers/fingerprint/fingerprint_extended_test.go @@ -0,0 +1,347 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package fingerprint + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFingerprint_Equals(t *testing.T) { //nolint:funlen + tests := []struct { + name string + fp1 Fingerprint + fp2 Fingerprint + expected bool + }{ + { + name: "identical fingerprints", + fp1: Fingerprint{ + FullHostname: "host.example.com", + Hostname: "host", + CloudProviderId: "i-12345", + DisplayName: "My Host", + BootID: "boot-123", + IpAddresses: Addresses{"eth0": {"192.168.1.1"}}, + MacAddresses: Addresses{"eth0": {"00:11:22:33:44:55"}}, + }, + fp2: Fingerprint{ + FullHostname: "host.example.com", + Hostname: "host", + CloudProviderId: "i-12345", + DisplayName: "My Host", + BootID: "boot-123", + IpAddresses: Addresses{"eth0": {"192.168.1.1"}}, + MacAddresses: Addresses{"eth0": {"00:11:22:33:44:55"}}, + }, + expected: true, + }, + { + name: "different hostname", + fp1: Fingerprint{ + FullHostname: "", + Hostname: "host1", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + fp2: Fingerprint{ + FullHostname: "", + Hostname: "host2", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + expected: false, + }, + { + name: "different full hostname", + fp1: Fingerprint{ + FullHostname: "host1.example.com", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + fp2: Fingerprint{ + FullHostname: "host2.example.com", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + expected: false, + }, + { + name: "different cloud provider id", + fp1: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "i-12345", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + fp2: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "i-67890", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + expected: false, + }, + { + name: "different boot id", + fp1: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "boot-123", + IpAddresses: nil, + MacAddresses: nil, + }, + fp2: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "boot-456", + IpAddresses: nil, + MacAddresses: nil, + }, + expected: false, + }, + { + name: "different display name", + fp1: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "Display 1", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + fp2: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "Display 2", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + expected: false, + }, + { + name: "different ip addresses", + fp1: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: Addresses{"eth0": {"192.168.1.1"}}, + MacAddresses: nil, + }, + fp2: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: Addresses{"eth0": {"192.168.1.2"}}, + MacAddresses: nil, + }, + expected: false, + }, + { + name: "different mac addresses", + fp1: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: Addresses{"eth0": {"00:11:22:33:44:55"}}, + }, + fp2: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: Addresses{"eth0": {"00:11:22:33:44:66"}}, + }, + expected: false, + }, + { + name: "empty fingerprints are equal", + fp1: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + fp2: Fingerprint{ + FullHostname: "", + Hostname: "", + CloudProviderId: "", + DisplayName: "", + BootID: "", + IpAddresses: nil, + MacAddresses: nil, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.fp1.Equals(tt.fp2) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestAddresses_Equals(t *testing.T) { + tests := []struct { + name string + a Addresses + b Addresses + expected bool + }{ + { + name: "both nil", + a: nil, + b: nil, + expected: true, + }, + { + name: "a nil b not nil", + a: nil, + b: Addresses{}, + expected: false, + }, + { + name: "a not nil b nil", + a: Addresses{}, + b: nil, + expected: false, + }, + { + name: "both empty", + a: Addresses{}, + b: Addresses{}, + expected: true, + }, + { + name: "same single entry", + a: Addresses{"eth0": {"192.168.1.1"}}, + b: Addresses{"eth0": {"192.168.1.1"}}, + expected: true, + }, + { + name: "different keys", + a: Addresses{"eth0": {"192.168.1.1"}}, + b: Addresses{"eth1": {"192.168.1.1"}}, + expected: false, + }, + { + name: "different values", + a: Addresses{"eth0": {"192.168.1.1"}}, + b: Addresses{"eth0": {"192.168.1.2"}}, + expected: false, + }, + { + name: "different number of keys", + a: Addresses{"eth0": {"192.168.1.1"}}, + b: Addresses{"eth0": {"192.168.1.1"}, "eth1": {"192.168.1.2"}}, + expected: false, + }, + { + name: "different number of values", + a: Addresses{"eth0": {"192.168.1.1"}}, + b: Addresses{"eth0": {"192.168.1.1", "192.168.1.2"}}, + expected: false, + }, + { + name: "same multiple entries", + a: Addresses{"eth0": {"192.168.1.1", "fe80::1"}, "lo": {"127.0.0.1"}}, + b: Addresses{"eth0": {"192.168.1.1", "fe80::1"}, "lo": {"127.0.0.1"}}, + expected: true, + }, + { + name: "same entries different order in slice", + a: Addresses{"eth0": {"192.168.1.1", "192.168.1.2"}}, + b: Addresses{"eth0": {"192.168.1.2", "192.168.1.1"}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.a.Equals(tt.b) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNewHarvestor(t *testing.T) { + tests := []struct { + name string + config any + expectError bool + }{ + { + name: "nil config returns error", + config: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + harvester, err := NewHarvestor(nil, nil, nil) + if tt.expectError { + require.Error(t, err) + assert.Nil(t, harvester) + } + }) + } +} + +func TestMockHarvestor_Harvest(t *testing.T) { + mock := &MockHarvestor{} + fingerprint, err := mock.Harvest() + + require.NoError(t, err) + assert.Equal(t, "test1.newrelic.com", fingerprint.FullHostname) + assert.Equal(t, "test1", fingerprint.Hostname) + assert.Equal(t, "1234abc", fingerprint.CloudProviderId) + assert.Equal(t, "foobar", fingerprint.DisplayName) + assert.Equal(t, "qwerty1234", fingerprint.BootID) + assert.NotNil(t, fingerprint.IpAddresses) + assert.NotNil(t, fingerprint.MacAddresses) +} diff --git a/pkg/helpers/helpers_extended_test.go b/pkg/helpers/helpers_extended_test.go new file mode 100644 index 000000000..064603c57 --- /dev/null +++ b/pkg/helpers/helpers_extended_test.go @@ -0,0 +1,367 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package helpers + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExpBackoff(t *testing.T) { + tests := []struct { + name string + base time.Duration + max time.Duration + count uint32 + expected time.Duration + }{ + { + name: "first retry", + base: time.Second, + max: time.Minute, + count: 1, + expected: 2 * time.Second, + }, + { + name: "second retry", + base: time.Second, + max: time.Minute, + count: 2, + expected: 3 * time.Second, + }, + { + name: "fifth retry", + base: time.Second, + max: time.Minute, + count: 5, + expected: 17 * time.Second, + }, + { + name: "exceeds max returns max", + base: time.Second, + max: 10 * time.Second, + count: 10, + expected: 10 * time.Second, + }, + { + name: "count at max boundary", + base: time.Second, + max: time.Hour, + count: MaxBackoffErrorCount, + expected: time.Hour, + }, + { + name: "count exceeds max boundary", + base: time.Second, + max: time.Hour, + count: MaxBackoffErrorCount + 1, + expected: time.Hour, + }, + { + name: "zero base", + base: 0, + max: time.Minute, + count: 3, + expected: 4 * time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExpBackoff(tt.base, tt.max, tt.count) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGetEnv(t *testing.T) { + tests := []struct { + name string + key string + envValue string + dfault string + combineWith []string + expected string + setEnv bool + }{ + { + name: "env not set returns default", + key: "TEST_NOT_SET_VAR", + envValue: "", + dfault: "default_value", + combineWith: nil, + expected: "default_value", + setEnv: false, + }, + { + name: "env set returns value", + key: "TEST_SET_VAR", + envValue: "env_value", + dfault: "default_value", + combineWith: nil, + expected: "env_value", + setEnv: true, + }, + { + name: "combine with single path", + key: "TEST_PATH_VAR", + envValue: "/base", + dfault: "/default", + combineWith: []string{"subdir"}, + expected: filepath.Join("/base", "subdir"), + setEnv: true, + }, + { + name: "combine with multiple paths", + key: "TEST_PATHS_VAR", + envValue: "/base", + dfault: "/default", + combineWith: []string{"sub1", "sub2", "file.txt"}, + expected: filepath.Join("/base", "sub1", "sub2", "file.txt"), + setEnv: true, + }, + { + name: "combine with default", + key: "TEST_COMBINE_DEFAULT", + envValue: "", + dfault: "/default", + combineWith: []string{"subdir"}, + expected: filepath.Join("/default", "subdir"), + setEnv: false, + }, + { + name: "empty env value uses default", + key: "TEST_EMPTY_VAR", + envValue: "", + dfault: "default_value", + combineWith: nil, + expected: "default_value", + setEnv: true, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + if testCase.setEnv { + t.Setenv(testCase.key, testCase.envValue) + } + + result := GetEnv(testCase.key, testCase.dfault, testCase.combineWith...) + assert.Equal(t, testCase.expected, result) + }) + } +} + +func TestFileMD5(t *testing.T) { + tests := []struct { + name string + content string + expectError bool + }{ + { + name: "hash simple content", + content: "hello world", + expectError: false, + }, + { + name: "hash empty file", + content: "", + expectError: false, + }, + { + name: "hash binary content", + content: string([]byte{0x00, 0x01, 0x02, 0xFF}), + expectError: false, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "testfile") + err := os.WriteFile(tmpFile, []byte(tt.content), 0o600) + require.NoError(t, err) + + hash, err := FileMD5(tmpFile) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Len(t, hash, 16) // MD5 produces 16 bytes + } + }) + } +} + +func TestFileMD5_FileNotFound(t *testing.T) { + hash, err := FileMD5("/nonexistent/path/file.txt") + require.Error(t, err) + assert.Nil(t, hash) +} + +func TestFileMD5_Consistency(t *testing.T) { + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "testfile") + content := "consistent content" + err := os.WriteFile(tmpFile, []byte(content), 0o600) + require.NoError(t, err) + + hash1, err := FileMD5(tmpFile) + require.NoError(t, err) + + hash2, err := FileMD5(tmpFile) + require.NoError(t, err) + + assert.Equal(t, hash1, hash2) +} + +func TestMaxBackoffErrorCount(t *testing.T) { + assert.Equal(t, 31, MaxBackoffErrorCount) +} + +func TestSanitizeFileNameCacheSize(t *testing.T) { + assert.Equal(t, 1000, SanitizeFileNameCacheSize) +} + +func TestHiddenField(t *testing.T) { + assert.Equal(t, "", HiddenField) +} + +func TestSensitiveKeys(t *testing.T) { + expectedKeys := []string{"key", "secret", "password", "token", "passphrase", "credential"} + assert.Equal(t, expectedKeys, SensitiveKeys) +} + +func TestJsonFilesRegexp(t *testing.T) { + tests := []struct { + name string + filename string + expected bool + }{ + { + name: "valid json file", + filename: "config.json", + expected: true, + }, + { + name: "json with path", + filename: "/path/to/config.json", + expected: true, + }, + { + name: "tilde prefix should not match", + filename: "~config.json", + expected: false, + }, + { + name: "swap file should not match", + filename: "config.json.swp", + expected: false, + }, + { + name: "not json extension", + filename: "config.yaml", + expected: false, + }, + { + name: "empty string", + filename: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := JsonFilesRegexp.MatchString(tt.filename) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestISO8601RE(t *testing.T) { + tests := []struct { + name string + input string + shouldMatch bool + }{ + { + name: "UTC timestamp", + input: "2015-06-29T16:04:53Z", + shouldMatch: true, + }, + { + name: "lowercase z", + input: "2015-06-29T16:04:53z", + shouldMatch: true, + }, + { + name: "positive offset", + input: "2015-06-29T16:04:53+07:00", + shouldMatch: true, + }, + { + name: "negative offset", + input: "2015-06-29T16:04:53-07:00", + shouldMatch: true, + }, + { + name: "not a timestamp", + input: "hello world", + shouldMatch: false, + }, + { + name: "partial timestamp", + input: "2015-06-29", + shouldMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ISO8601RE.MatchString(tt.input) + assert.Equal(t, tt.shouldMatch, result) + }) + } +} + +func TestObfuscateSensitiveDataFromString(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "no sensitive data", + input: "hello world", + expected: "hello world", + }, + { + name: "password in command", + input: "-password=secret123", + expected: "-password=", + }, + { + name: "token in env var", + input: "MY_TOKEN=abc123", + expected: "MY_TOKEN=", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ObfuscateSensitiveDataFromString(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/helpers/io_test.go b/pkg/helpers/io_test.go new file mode 100644 index 000000000..18d5e387f --- /dev/null +++ b/pkg/helpers/io_test.go @@ -0,0 +1,212 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package helpers + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCopyFile(t *testing.T) { + tests := []struct { + name string + srcContent string + destExists bool + expectError bool + }{ + { + name: "copy to new file", + srcContent: "test content", + destExists: false, + expectError: false, + }, + { + name: "overwrite existing file", + srcContent: "new content", + destExists: true, + expectError: false, + }, + { + name: "copy empty file", + srcContent: "", + destExists: false, + expectError: false, + }, + { + name: "copy large content", + srcContent: string(make([]byte, 10000)), + destExists: false, + expectError: false, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + srcFile := filepath.Join(tmpDir, "source.txt") + destFile := filepath.Join(tmpDir, "dest.txt") + + // Create source file + err := os.WriteFile(srcFile, []byte(tt.srcContent), 0o600) + require.NoError(t, err) + + // Create dest file if needed + if tt.destExists { + err = os.WriteFile(destFile, []byte("old content"), 0o600) + require.NoError(t, err) + } + + // Execute copy + err = CopyFile(srcFile, destFile) + if tt.expectError { + assert.Error(t, err) + + return + } + + require.NoError(t, err) + + // Verify content + destContent, err := os.ReadFile(destFile) + require.NoError(t, err) + assert.Equal(t, tt.srcContent, string(destContent)) + }) + } +} + +func TestCopyFile_SourceNotFound(t *testing.T) { + tmpDir := t.TempDir() + srcFile := filepath.Join(tmpDir, "nonexistent.txt") + destFile := filepath.Join(tmpDir, "dest.txt") + + err := CopyFile(srcFile, destFile) + require.Error(t, err) +} + +func TestCopyFile_DestIsDirectory(t *testing.T) { + tmpDir := t.TempDir() + srcFile := filepath.Join(tmpDir, "source.txt") + destDir := filepath.Join(tmpDir, "destdir") + + // Create source file + err := os.WriteFile(srcFile, []byte("test"), 0o600) + require.NoError(t, err) + + // Create destination directory + err = os.Mkdir(destDir, 0o755) + require.NoError(t, err) + + err = CopyFile(srcFile, destDir) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot copy file") +} + +func TestCopyFile_PreservesPermissions(t *testing.T) { + if GetOS() == OS_WINDOWS { + t.Skip("Skipping permission test on Windows") + } + + tmpDir := t.TempDir() + srcFile := filepath.Join(tmpDir, "source.sh") + destFile := filepath.Join(tmpDir, "dest.sh") + + // Create source file with execute permission + err := os.WriteFile(srcFile, []byte("#!/bin/bash\necho hello"), 0o700) //nolint:gosec + require.NoError(t, err) + + err = CopyFile(srcFile, destFile) + require.NoError(t, err) + + // Verify permissions preserved + srcInfo, err := os.Stat(srcFile) + require.NoError(t, err) + destInfo, err := os.Stat(destFile) + require.NoError(t, err) + + assert.Equal(t, srcInfo.Mode().Perm(), destInfo.Mode().Perm()) +} + +func TestFileExists(t *testing.T) { + tmpDir := t.TempDir() + + tests := []struct { + name string + setup func() string + expected bool + }{ + { + name: "existing file", + setup: func() string { + path := filepath.Join(tmpDir, "exists.txt") + _ = os.WriteFile(path, []byte("test"), 0o600) + + return path + }, + expected: true, + }, + { + name: "nonexistent file", + setup: func() string { + return filepath.Join(tmpDir, "nonexistent.txt") + }, + expected: false, + }, + { + name: "existing directory", + setup: func() string { + path := filepath.Join(tmpDir, "existsdir") + _ = os.Mkdir(path, 0o755) + + return path + }, + expected: true, + }, + { + name: "empty filename", + setup: func() string { + return "" + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := tt.setup() + result := FileExists(path) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFileExists_SymbolicLink(t *testing.T) { + if GetOS() == OS_WINDOWS { + t.Skip("Skipping symlink test on Windows") + } + + tmpDir := t.TempDir() + realFile := filepath.Join(tmpDir, "real.txt") + symlink := filepath.Join(tmpDir, "symlink.txt") + + // Create real file + err := os.WriteFile(realFile, []byte("test"), 0o600) + require.NoError(t, err) + + // Create symlink + err = os.Symlink(realFile, symlink) + require.NoError(t, err) + + assert.True(t, FileExists(symlink)) + + // Remove real file - broken symlink + err = os.Remove(realFile) + require.NoError(t, err) + + // Broken symlink should still return false (Stat follows symlinks) + assert.False(t, FileExists(symlink)) +} diff --git a/pkg/ipc/messages_test.go b/pkg/ipc/messages_test.go new file mode 100644 index 000000000..e2758a6e8 --- /dev/null +++ b/pkg/ipc/messages_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ipc + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMessage_String(t *testing.T) { + tests := []struct { + name string + msg Message + expected string + }{ + { + name: "custom message", + msg: Message("custom"), + expected: "custom", + }, + { + name: "empty message", + msg: Message(""), + expected: "", + }, + { + name: "message with spaces", + msg: Message("hello world"), + expected: "hello world", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, string(tt.msg)) + }) + } +} + +func TestMessage_TypeConversion(t *testing.T) { + // Test that Message can be used as a string + msg := Message("test") + s := string(msg) + assert.Equal(t, "test", s) + + // Test that string can be converted to Message + str := "another test" + m := Message(str) + assert.Equal(t, Message("another test"), m) +} + +func TestMessage_Equality(t *testing.T) { + msg1 := Message("test") + msg2 := Message("test") + msg3 := Message("different") + + assert.Equal(t, msg1, msg2) + assert.NotEqual(t, msg1, msg3) +} + +func TestMessage_EmptyMessage(t *testing.T) { + var empty Message + assert.Equal(t, Message(""), empty) + assert.Empty(t, string(empty)) +} + +func TestMessage_MapKey(t *testing.T) { + // Test that Message can be used as a map key + handlers := make(map[Message]func() error) + + handlers[Message("test")] = func() error { return nil } + handlers[Message("another")] = func() error { return nil } + + assert.Len(t, handlers, 2) + assert.NotNil(t, handlers[Message("test")]) + assert.NotNil(t, handlers[Message("another")]) + assert.Nil(t, handlers[Message("nonexistent")]) +} diff --git a/pkg/license/license_test.go b/pkg/license/license_test.go index 93700abc3..4407deaaf 100644 --- a/pkg/license/license_test.go +++ b/pkg/license/license_test.go @@ -4,19 +4,192 @@ package license import ( - "gotest.tools/assert" "testing" + + "github.com/stretchr/testify/assert" ) const ( basic = "0123456789012345678901234567890123456789" eu = "eu01xx6789012345678901234567890123456789" + gov = "gov123456789012345678901234567890123456789" ) func TestLicense_GetRegion(t *testing.T) { - region := GetRegion(basic) - assert.Equal(t, region, "") + tests := []struct { + name string + license string + expected string + }{ + { + name: "basic license has no region", + license: basic, + expected: "", + }, + { + name: "eu license", + license: eu, + expected: "eu", + }, + { + name: "gov license", + license: gov, + expected: "gov", + }, + { + name: "empty license", + license: "", + expected: "", + }, + { + name: "short license", + license: "ab", + expected: "ab", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + region := GetRegion(tt.license) + assert.Equal(t, tt.expected, region) + }) + } +} + +func TestLicense_IsValid(t *testing.T) { + tests := []struct { + name string + license string + expected bool + }{ + { + name: "valid alphanumeric license", + license: basic, + expected: true, + }, + { + name: "valid eu license", + license: eu, + expected: true, + }, + { + name: "valid gov license", + license: gov, + expected: true, + }, + { + name: "empty license is invalid", + license: "", + expected: false, + }, + { + name: "license with special characters is invalid", + license: "abc-123-def", + expected: false, + }, + { + name: "license with spaces is invalid", + license: "abc 123 def", + expected: false, + }, + { + name: "license with underscores is invalid", + license: "abc_123_def", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValid(tt.license) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestLicense_IsRegionEU(t *testing.T) { + tests := []struct { + name string + license string + expected bool + }{ + { + name: "basic license is not EU", + license: basic, + expected: false, + }, + { + name: "eu license is EU", + license: eu, + expected: true, + }, + { + name: "gov license is not EU", + license: gov, + expected: false, + }, + { + name: "empty license is not EU", + license: "", + expected: false, + }, + { + name: "eu01 prefix is EU", + license: "eu01abcdefghijklmnop", + expected: true, + }, + { + name: "single char is not EU", + license: "e", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsRegionEU(tt.license) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestLicense_IsFederalCompliance(t *testing.T) { + tests := []struct { + name string + license string + expected bool + }{ + { + name: "basic license is not federal", + license: basic, + expected: false, + }, + { + name: "eu license is not federal", + license: eu, + expected: false, + }, + { + name: "gov license is federal", + license: gov, + expected: true, + }, + { + name: "empty license is not federal", + license: "", + expected: false, + }, + { + name: "license starting with gov is federal", + license: "gov0123456789", + expected: true, + }, + } - region = GetRegion(eu) - assert.Equal(t, region, "eu") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsFederalCompliance(tt.license) + assert.Equal(t, tt.expected, result) + }) + } } diff --git a/pkg/metrics/types/process_sample_test.go b/pkg/metrics/types/process_sample_test.go new file mode 100644 index 000000000..1863d0f7a --- /dev/null +++ b/pkg/metrics/types/process_sample_test.go @@ -0,0 +1,207 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "encoding/json" + "testing" + + "github.com/newrelic/infrastructure-agent/pkg/entity" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFlatProcessSample_Type(t *testing.T) { + tests := []struct { + name string + eventType string + }{ + { + name: "set process sample type", + eventType: "ProcessSample", + }, + { + name: "empty event type", + eventType: "", + }, + { + name: "custom event type", + eventType: "CustomProcess", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fps := &FlatProcessSample{} + fps.Type(tt.eventType) + assert.Equal(t, tt.eventType, (*fps)["eventType"]) + }) + } +} + +func TestFlatProcessSample_Entity(t *testing.T) { + tests := []struct { + name string + entityKey entity.Key + }{ + { + name: "standard entity key", + entityKey: entity.Key("host:my-hostname"), + }, + { + name: "empty entity key", + entityKey: entity.Key(""), + }, + { + name: "container entity key", + entityKey: entity.Key("container:abc123"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fps := &FlatProcessSample{} + fps.Entity(tt.entityKey) + assert.Equal(t, tt.entityKey, (*fps)["entityKey"]) + }) + } +} + +func TestFlatProcessSample_Timestamp(t *testing.T) { + tests := []struct { + name string + timestamp int64 + }{ + { + name: "positive timestamp", + timestamp: 1609459200000, + }, + { + name: "zero timestamp", + timestamp: 0, + }, + { + name: "negative timestamp", + timestamp: -1000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fps := &FlatProcessSample{} + fps.Timestamp(tt.timestamp) + assert.Equal(t, tt.timestamp, (*fps)["timestamp"]) + }) + } +} + +func TestFlatProcessSample_ImplementsEventInterface(_ *testing.T) { + var _ interface { + Type(eventType string) + Entity(key entity.Key) + Timestamp(timestamp int64) + } = &FlatProcessSample{} +} + +func TestProcessSample_Fields(t *testing.T) { + fdCount := int32(100) + ioReadCount := float64(1000) + + sample := ProcessSample{ //nolint:exhaustruct + ProcessDisplayName: "nginx", + ProcessID: 1234, + CommandName: "nginx", + User: "root", + MemoryRSSBytes: 1024000, + MemoryVMSBytes: 2048000, + CPUPercent: 5.5, + CPUUserPercent: 3.0, + CPUSystemPercent: 2.5, + ContainerImage: "nginx:latest", + ContainerImageName: "nginx", + ContainerName: "my-nginx", + ContainerID: "abc123def456", + Contained: "true", + CmdLine: "nginx -g daemon off;", + Status: "running", + ParentProcessID: 1, + ThreadCount: 4, + FdCount: &fdCount, + IOReadCountPerSecond: &ioReadCount, + } + + assert.Equal(t, "nginx", sample.ProcessDisplayName) + assert.Equal(t, int32(1234), sample.ProcessID) + assert.Equal(t, "nginx", sample.CommandName) + assert.Equal(t, "root", sample.User) + assert.Equal(t, int64(1024000), sample.MemoryRSSBytes) + assert.Equal(t, int64(2048000), sample.MemoryVMSBytes) + assert.InEpsilon(t, 5.5, sample.CPUPercent, 0.001) + assert.InEpsilon(t, 3.0, sample.CPUUserPercent, 0.001) + assert.InEpsilon(t, 2.5, sample.CPUSystemPercent, 0.001) + assert.Equal(t, "nginx:latest", sample.ContainerImage) + assert.Equal(t, "nginx", sample.ContainerImageName) + assert.Equal(t, "my-nginx", sample.ContainerName) + assert.Equal(t, "abc123def456", sample.ContainerID) + assert.Equal(t, "true", sample.Contained) + assert.Equal(t, "nginx -g daemon off;", sample.CmdLine) + assert.Equal(t, "running", sample.Status) + assert.Equal(t, int32(1), sample.ParentProcessID) + assert.Equal(t, int32(4), sample.ThreadCount) + assert.Equal(t, int32(100), *sample.FdCount) + assert.InEpsilon(t, float64(1000), *sample.IOReadCountPerSecond, 0.001) +} + +func TestProcessSample_JSONMarshaling(t *testing.T) { + sample := ProcessSample{ //nolint:exhaustruct + ProcessDisplayName: "test-process", + ProcessID: 42, + CPUPercent: 10.5, + } + + data, err := json.Marshal(sample) + require.NoError(t, err) + + var result map[string]any + + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + assert.Equal(t, "test-process", result["processDisplayName"]) + assert.InEpsilon(t, float64(42), result["processId"], 0.001) + assert.InEpsilon(t, 10.5, result["cpuPercent"], 0.001) +} + +func TestProcessSample_OmitEmptyFields(t *testing.T) { + sample := ProcessSample{ //nolint:exhaustruct + ProcessDisplayName: "test", + ProcessID: 1, + } + + data, err := json.Marshal(sample) + require.NoError(t, err) + + var result map[string]any + + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + // Fields with omitempty should not be present when empty + _, hasContainerID := result["containerId"] + assert.False(t, hasContainerID) + + _, hasUser := result["userName"] + assert.False(t, hasUser) +} + +func TestFlatProcessSample_MapBehavior(t *testing.T) { + fps := FlatProcessSample{} + + fps["customField"] = "customValue" + fps["intField"] = 42 + + assert.Equal(t, "customValue", fps["customField"]) + assert.Equal(t, 42, fps["intField"]) + assert.Nil(t, fps["nonexistent"]) +} diff --git a/pkg/plugins/ids/plugin_ids_test.go b/pkg/plugins/ids/plugin_ids_test.go new file mode 100644 index 000000000..efa1bf9c4 --- /dev/null +++ b/pkg/plugins/ids/plugin_ids_test.go @@ -0,0 +1,322 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package ids + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewPluginID(t *testing.T) { + tests := []struct { + name string + category string + term string + }{ + { + name: "standard plugin id", + category: "metadata", + term: "system", + }, + { + name: "empty category", + category: "", + term: "system", + }, + { + name: "empty term", + category: "metadata", + term: "", + }, + { + name: "empty both", + category: "", + term: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := NewPluginID(tt.category, tt.term) + assert.Equal(t, tt.category, id.Category) + assert.Equal(t, tt.term, id.Term) + }) + } +} + +func TestNewDefaultInventoryPluginID(t *testing.T) { + tests := []struct { + name string + term string + }{ + { + name: "standard term", + term: "myintegration", + }, + { + name: "empty term", + term: "", + }, + { + name: "term with special characters", + term: "my-integration_v1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := NewDefaultInventoryPluginID(tt.term) + assert.Equal(t, DefaultInventoryCategory, id.Category) + assert.Equal(t, tt.term, id.Term) + }) + } +} + +func TestFromString(t *testing.T) { + tests := []struct { + name string + source string + expected PluginID + expectError bool + }{ + { + name: "valid source", + source: "metadata/system", + expected: PluginID{Category: "metadata", Term: "system"}, + expectError: false, + }, + { + name: "valid source with special characters", + source: "my-category/my_term", + expected: PluginID{Category: "my-category", Term: "my_term"}, + expectError: false, + }, + { + name: "invalid source no separator", + source: "metadatasystem", + expected: PluginID{Category: "", Term: ""}, + expectError: true, + }, + { + name: "invalid source multiple separators", + source: "meta/data/system", + expected: PluginID{Category: "", Term: ""}, + expectError: true, + }, + { + name: "empty source", + source: "", + expected: PluginID{Category: "", Term: ""}, + expectError: true, + }, + { + name: "only separator", + source: "/", + expected: PluginID{Category: "", Term: ""}, + expectError: false, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + result, err := FromString(tt.source) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestPluginID_String(t *testing.T) { + tests := []struct { + name string + id PluginID + expected string + }{ + { + name: "standard plugin id", + id: PluginID{Category: "metadata", Term: "system"}, + expected: "metadata/system", + }, + { + name: "empty category", + id: PluginID{Category: "", Term: "system"}, + expected: "/system", + }, + { + name: "empty term", + id: PluginID{Category: "metadata", Term: ""}, + expected: "metadata/", + }, + { + name: "empty both", + id: PluginID{Category: "", Term: ""}, + expected: "/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.id.String()) + }) + } +} + +func TestPluginID_SortKey(t *testing.T) { + tests := []struct { + name string + id PluginID + }{ + { + name: "sort key equals string", + id: PluginID{Category: "metadata", Term: "system"}, + }, + { + name: "empty plugin id", + id: PluginID{Category: "", Term: ""}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.id.String(), tt.id.SortKey()) + }) + } +} + +func TestPluginID_MarshalJSON(t *testing.T) { + tests := []struct { + name string + id PluginID + expected string + }{ + { + name: "standard plugin id", + id: PluginID{Category: "metadata", Term: "system"}, + expected: `"metadata/system"`, + }, + { + name: "empty plugin id", + id: PluginID{Category: "", Term: ""}, + expected: `"/"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.id) + require.NoError(t, err) + assert.Equal(t, tt.expected, string(data)) + }) + } +} + +func TestPluginID_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + jsonData string + expected PluginID + expectError bool + }{ + { + name: "standard plugin id", + jsonData: `"metadata/system"`, + expected: PluginID{Category: "metadata", Term: "system"}, + expectError: false, + }, + { + name: "empty category", + jsonData: `"/system"`, + expected: PluginID{Category: "", Term: "system"}, + expectError: false, + }, + { + name: "invalid format", + jsonData: `"metadatasystem"`, + expected: PluginID{Category: "", Term: ""}, + expectError: true, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + var result PluginID + + err := json.Unmarshal([]byte(tt.jsonData), &result) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestPluginID_UnmarshalYAML(t *testing.T) { + tests := []struct { + name string + value string + expected PluginID + expectError bool + }{ + { + name: "standard plugin id", + value: "metadata/system", + expected: PluginID{Category: "metadata", Term: "system"}, + expectError: false, + }, + { + name: "invalid format", + value: "metadatasystem", + expected: PluginID{Category: "", Term: ""}, + expectError: true, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + var result PluginID + + err := result.UnmarshalYAML(func(v any) error { + strPtr, ok := v.(*string) + if !ok { + t.Fatal("expected *string type") + } + + *strPtr = tt.value + + return nil + }) + + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestPredefinedPluginIDs(t *testing.T) { + assert.Equal(t, "metadata", CustomAttrsID.Category) + assert.Equal(t, "attributes", CustomAttrsID.Term) + + assert.Equal(t, "metadata", HostInfo.Category) + assert.Equal(t, "system", HostInfo.Term) + + assert.Empty(t, EmptyInventorySource.Category) + assert.Empty(t, EmptyInventorySource.Term) +} + +func TestDefaultInventoryCategory(t *testing.T) { + assert.Equal(t, "integration", DefaultInventoryCategory) +} diff --git a/pkg/plugins/integrations_only_test.go b/pkg/plugins/integrations_only_test.go new file mode 100644 index 000000000..9409b281d --- /dev/null +++ b/pkg/plugins/integrations_only_test.go @@ -0,0 +1,43 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plugins + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIntegrationsOnly_SortKey(t *testing.T) { + tests := []struct { + name string + io IntegrationsOnly + expected string + }{ + { + name: "true value", + io: IntegrationsOnly(true), + expected: "integrations_only", + }, + { + name: "false value", + io: IntegrationsOnly(false), + expected: "integrations_only", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.io.SortKey()) + }) + } +} + +func TestIntegrationsOnly_BoolValue(t *testing.T) { + trueValue := IntegrationsOnly(true) + falseValue := IntegrationsOnly(false) + + assert.True(t, bool(trueValue)) + assert.False(t, bool(falseValue)) +} diff --git a/pkg/plugins/proxy/proxy_config_test.go b/pkg/plugins/proxy/proxy_config_test.go new file mode 100644 index 000000000..019178092 --- /dev/null +++ b/pkg/plugins/proxy/proxy_config_test.go @@ -0,0 +1,226 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package proxy + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUrlEntry(t *testing.T) { + tests := []struct { + name string + rawURL string + expectedScheme string + expectedError string + isNil bool + }{ + { + name: "https url", + rawURL: "https://proxy.example.com:8080", + expectedScheme: "https", + expectedError: "", + isNil: false, + }, + { + name: "http url", + rawURL: "http://proxy.example.com:8080", + expectedScheme: "http", + expectedError: "", + isNil: false, + }, + { + name: "socks5 url", + rawURL: "socks5://proxy.example.com:1080", + expectedScheme: "socks5", + expectedError: "", + isNil: false, + }, + { + name: "empty url returns nil", + rawURL: "", + expectedScheme: "", + expectedError: "", + isNil: true, + }, + { + name: "url with auth", + rawURL: "http://user:pass@proxy.example.com:8080", + expectedScheme: "http", + expectedError: "", + isNil: false, + }, + { + name: "url with path", + rawURL: "http://proxy.example.com:8080/path", + expectedScheme: "http", + expectedError: "", + isNil: false, + }, + { + name: "invalid url", + rawURL: "://invalid", + expectedScheme: "", + expectedError: "wrong url", + isNil: false, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + result := urlEntry(tt.rawURL) + if tt.isNil { + assert.Nil(t, result) + } else { + assert.NotNil(t, result) + assert.Equal(t, tt.expectedScheme, result.Scheme) + assert.Equal(t, tt.expectedError, result.Error) + } + }) + } +} + +func TestPathEntry(t *testing.T) { + // Create temp directory and file for testing + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "ca-bundle.crt") + err := os.WriteFile(tmpFile, []byte("test"), 0o600) + require.NoError(t, err) + + tests := []struct { + name string + path string + expectedType string + isNil bool + }{ + { + name: "empty path returns nil", + path: "", + expectedType: "", + isNil: true, + }, + { + name: "directory path", + path: tmpDir, + expectedType: typeDir, + isNil: false, + }, + { + name: "file path", + path: tmpFile, + expectedType: typeFile, + isNil: false, + }, + { + name: "nonexistent path", + path: "/nonexistent/path", + expectedType: "unexpected error", + isNil: false, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + result := pathEntry(tt.path) + if tt.isNil { + assert.Nil(t, result) + } else { + assert.NotNil(t, result) + assert.Equal(t, tt.expectedType, result.Type) + } + }) + } +} + +func TestEntry_SortKey(t *testing.T) { + tests := []struct { + name string + entry entry + expected string + }{ + { + name: "standard id", + entry: entry{Id: "proxy"}, + expected: "proxy", + }, + { + name: "empty id", + entry: entry{Id: ""}, + expected: "", + }, + { + name: "id with special characters", + entry: entry{Id: "ca_bundle_file"}, + expected: "ca_bundle_file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.entry.SortKey()) + }) + } +} + +func TestProxyEntry_Fields(t *testing.T) { + proxyE := proxyEntry{ + entry: entry{Id: "test_proxy"}, + Scheme: "https", + Error: "", + } + + assert.Equal(t, "test_proxy", proxyE.Id) + assert.Equal(t, "https", proxyE.Scheme) + assert.Empty(t, proxyE.Error) +} + +func TestFileEntry_Fields(t *testing.T) { + fileE := fileEntry{ + entry: entry{Id: "ca_bundle"}, + Type: typeFile, + } + + assert.Equal(t, "ca_bundle", fileE.Id) + assert.Equal(t, typeFile, fileE.Type) +} + +func TestBoolEntry_Fields(t *testing.T) { + tests := []struct { + name string + entry boolEntry + expected bool + }{ + { + name: "true value", + entry: boolEntry{ + entry: entry{Id: "ignore_proxy"}, + Value: true, + }, + expected: true, + }, + { + name: "false value", + entry: boolEntry{ + entry: entry{Id: "validate_certs"}, + Value: false, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.entry.Value) + }) + } +} + +func TestTypeConstants(t *testing.T) { + assert.Equal(t, "file", typeFile) + assert.Equal(t, "directory", typeDir) +} diff --git a/pkg/sample/sample_test.go b/pkg/sample/sample_test.go new file mode 100644 index 000000000..df7fc8585 --- /dev/null +++ b/pkg/sample/sample_test.go @@ -0,0 +1,240 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package sample + +import ( + "encoding/json" + "testing" + + "github.com/newrelic/infrastructure-agent/pkg/entity" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBaseEvent_Type(t *testing.T) { + tests := []struct { + name string + eventType string + }{ + { + name: "standard event type", + eventType: "SystemSample", + }, + { + name: "custom event type", + eventType: "CustomEvent", + }, + { + name: "empty event type", + eventType: "", + }, + { + name: "event type with special characters", + eventType: "Event_Type-123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BaseEvent{EventType: "", Timestmp: 0, EntityKey: ""} + be.Type(tt.eventType) + assert.Equal(t, tt.eventType, be.EventType) + }) + } +} + +func TestBaseEvent_Entity(t *testing.T) { + tests := []struct { + name string + entityKey entity.Key + expectedKey string + }{ + { + name: "standard entity key", + entityKey: entity.Key("host:my-hostname"), + expectedKey: "host:my-hostname", + }, + { + name: "empty entity key", + entityKey: entity.Key(""), + expectedKey: "", + }, + { + name: "entity key with special characters", + entityKey: entity.Key("container:abc-123_def"), + expectedKey: "container:abc-123_def", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BaseEvent{EventType: "", Timestmp: 0, EntityKey: ""} + be.Entity(tt.entityKey) + assert.Equal(t, tt.expectedKey, be.EntityKey) + }) + } +} + +func TestBaseEvent_Timestamp(t *testing.T) { + tests := []struct { + name string + timestamp int64 + }{ + { + name: "positive timestamp", + timestamp: 1609459200000, + }, + { + name: "zero timestamp", + timestamp: 0, + }, + { + name: "negative timestamp", + timestamp: -1000, + }, + { + name: "max int64 timestamp", + timestamp: 9223372036854775807, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + be := &BaseEvent{EventType: "", Timestmp: 0, EntityKey: ""} + be.Timestamp(tt.timestamp) + assert.Equal(t, tt.timestamp, be.Timestmp) + }) + } +} + +func TestBaseEvent_JSONMarshaling(t *testing.T) { + tests := []struct { + name string + event BaseEvent + expected map[string]any + }{ + { + name: "full event", + event: BaseEvent{ + EventType: "SystemSample", + Timestmp: 1609459200000, + EntityKey: "host:my-host", + }, + expected: map[string]any{ + "eventType": "SystemSample", + "timestamp": float64(1609459200000), + "entityKey": "host:my-host", + }, + }, + { + name: "empty event", + event: BaseEvent{EventType: "", Timestmp: 0, EntityKey: ""}, + expected: map[string]any{ + "eventType": "", + "timestamp": float64(0), + "entityKey": "", + }, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.event) + require.NoError(t, err) + + var result map[string]any + + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + assert.Equal(t, tt.expected["eventType"], result["eventType"]) + assert.Equal(t, tt.expected["timestamp"], result["timestamp"]) + assert.Equal(t, tt.expected["entityKey"], result["entityKey"]) + }) + } +} + +func TestBaseEvent_JSONUnmarshaling(t *testing.T) { + tests := []struct { + name string + jsonData string + expected BaseEvent + }{ + { + name: "full event", + jsonData: `{"eventType":"SystemSample","timestamp":1609459200000,"entityKey":"host:my-host"}`, + expected: BaseEvent{ + EventType: "SystemSample", + Timestmp: 1609459200000, + EntityKey: "host:my-host", + }, + }, + { + name: "empty event", + jsonData: `{}`, + expected: BaseEvent{EventType: "", Timestmp: 0, EntityKey: ""}, + }, + } + + for _, tt := range tests { //nolint:varnamelen + t.Run(tt.name, func(t *testing.T) { + var result BaseEvent + + err := json.Unmarshal([]byte(tt.jsonData), &result) + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBaseEvent_ImplementsEventInterface(_ *testing.T) { + var _ Event = (*BaseEvent)(nil) +} + +func TestEventBatch(t *testing.T) { + tests := []struct { + name string + batch EventBatch + len int + }{ + { + name: "empty batch", + batch: EventBatch{}, + len: 0, + }, + { + name: "single event batch", + batch: EventBatch{ + &BaseEvent{EventType: "Event1", Timestmp: 0, EntityKey: ""}, + }, + len: 1, + }, + { + name: "multiple events batch", + batch: EventBatch{ + &BaseEvent{EventType: "Event1", Timestmp: 0, EntityKey: ""}, + &BaseEvent{EventType: "Event2", Timestmp: 0, EntityKey: ""}, + &BaseEvent{EventType: "Event3", Timestmp: 0, EntityKey: ""}, + }, + len: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Len(t, tt.batch, tt.len) + }) + } +} + +func TestBaseEvent_ChainedCalls(t *testing.T) { + baseEvent := &BaseEvent{EventType: "", Timestmp: 0, EntityKey: ""} + baseEvent.Type("TestEvent") + baseEvent.Entity(entity.Key("host:test")) + baseEvent.Timestamp(1234567890) + + assert.Equal(t, "TestEvent", baseEvent.EventType) + assert.Equal(t, "host:test", baseEvent.EntityKey) + assert.Equal(t, int64(1234567890), baseEvent.Timestmp) +} diff --git a/pkg/sysinfo/types_test.go b/pkg/sysinfo/types_test.go new file mode 100644 index 000000000..4c1cba777 --- /dev/null +++ b/pkg/sysinfo/types_test.go @@ -0,0 +1,110 @@ +// Copyright 2026 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package sysinfo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHostAliases_SortKey(t *testing.T) { + tests := []struct { + name string + alias HostAliases + expected string + }{ + { + name: "hostname source", + alias: HostAliases{ + Alias: "my-host.example.com", + Source: HOST_SOURCE_HOSTNAME, + }, + expected: HOST_SOURCE_HOSTNAME, + }, + { + name: "display name source", + alias: HostAliases{ + Alias: "My Display Name", + Source: HOST_SOURCE_DISPLAY_NAME, + }, + expected: HOST_SOURCE_DISPLAY_NAME, + }, + { + name: "instance id source", + alias: HostAliases{ + Alias: "i-1234567890abcdef0", + Source: HOST_SOURCE_INSTANCE_ID, + }, + expected: HOST_SOURCE_INSTANCE_ID, + }, + { + name: "empty source", + alias: HostAliases{ + Alias: "some-alias", + Source: "", + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.alias.SortKey()) + }) + } +} + +func TestHostSourceConstants(t *testing.T) { + assert.Equal(t, "display_name", HOST_SOURCE_DISPLAY_NAME) + assert.Equal(t, "instance-id", HOST_SOURCE_INSTANCE_ID) + assert.Equal(t, "azure_vm_id", HOST_SOURCE_AZURE_VM_ID) + assert.Equal(t, "gcp_vm_id", HOST_SOURCE_GCP_VM_ID) + assert.Equal(t, "alibaba_vm_id", HOST_SOURCE_ALIBABA_VM_ID) + assert.Equal(t, "oci_vm_id", HOST_SOURCE_OCI_VM_ID) + assert.Equal(t, "hostname", HOST_SOURCE_HOSTNAME) + assert.Equal(t, "hostname_short", HOST_SOURCE_HOSTNAME_SHORT) +} + +func TestProcessNameSourceConstants(t *testing.T) { + assert.Equal(t, "daemontools", PROCESS_NAME_SOURCE_DAEMONTOOLS) + assert.Equal(t, "supervisor", PROCESS_NAME_SOURCE_SUPERVISOR) + assert.Equal(t, "systemd", PROCESS_NAME_SOURCE_SYSTEMD) + assert.Equal(t, "sysvinit", PROCESS_NAME_SOURCE_SYSVINIT) + assert.Equal(t, "upstart", PROCESS_NAME_SOURCE_UPSTART) +} + +func TestHostIDTypes(t *testing.T) { + expected := []string{ + HOST_SOURCE_INSTANCE_ID, + HOST_SOURCE_AZURE_VM_ID, + HOST_SOURCE_GCP_VM_ID, + HOST_SOURCE_ALIBABA_VM_ID, + HOST_SOURCE_OCI_VM_ID, + HOST_SOURCE_DISPLAY_NAME, + HOST_SOURCE_HOSTNAME, + } + assert.Equal(t, expected, HOST_ID_TYPES) +} + +func TestProcessNameSources(t *testing.T) { + expected := []string{ + PROCESS_NAME_SOURCE_DAEMONTOOLS, + PROCESS_NAME_SOURCE_SUPERVISOR, + PROCESS_NAME_SOURCE_SYSTEMD, + PROCESS_NAME_SOURCE_UPSTART, + PROCESS_NAME_SOURCE_SYSVINIT, + } + assert.Equal(t, expected, PROCESS_NAME_SOURCES) +} + +func TestHostAliases_Fields(t *testing.T) { + alias := HostAliases{ + Alias: "test-alias", + Source: "test-source", + } + + assert.Equal(t, "test-alias", alias.Alias) + assert.Equal(t, "test-source", alias.Source) +}