Files
activecm-rita/util/util_test.go
2024-09-12 21:03:22 -06:00

1410 lines
36 KiB
Go

package util
import (
"crypto/md5" // #nosec G501
"database/sql/driver"
"fmt"
"math"
"net"
"net/http"
"net/http/httptest"
"os"
"path"
"testing"
"time"
"github.com/google/go-github/github"
"github.com/google/uuid"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
func TestNewFixedStringHash(t *testing.T) {
tests := []struct {
name string
args []string
expected FixedString
expectedErr bool
}{
{
name: "Single string",
args: []string{"hello"},
expected: FixedString{
// #nosec G401 : this md5 is used for hashing, not for security
Data: md5.Sum([]byte("hello")),
},
expectedErr: false,
},
{
name: "Multiple strings",
args: []string{"hello", "world"},
expected: FixedString{
Data: md5.Sum([]byte("helloworld")), // #nosec G401
},
expectedErr: false,
},
{
name: "Combination of strings",
args: []string{"foo", "bar", "baz"},
expected: FixedString{
Data: md5.Sum([]byte("foobarbaz")), // #nosec G401
},
expectedErr: false,
},
{
name: "Whitespace strings",
args: []string{" ", " "},
expected: FixedString{
Data: md5.Sum([]byte(" ")), // #nosec G401
},
expectedErr: false,
},
{
name: "Empty string",
args: []string{""},
expected: FixedString{
Data: md5.Sum([]byte("")), // #nosec G401
},
expectedErr: true,
},
{
name: "Multiple empty strings",
args: []string{"", ""},
expected: FixedString{
Data: md5.Sum([]byte("")), // #nosec G401
},
expectedErr: true,
},
{
name: "No arguments",
args: []string{},
expected: FixedString{},
expectedErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := NewFixedStringHash(test.args...)
if test.expectedErr {
require.Error(t, err, "error was expected")
} else {
require.NoError(t, err, "generating hash should not produce an error")
require.Equal(t, test.expected, result, "hash should match expected value")
}
})
}
}
func TestNewFixedStringFromHex(t *testing.T) {
tests := []struct {
name string
input string
expected FixedString
expectedError error
}{
{
name: "Valid Hex String",
input: "00112233445566778899aabbccddeeff",
expected: FixedString{
Data: [16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
},
expectedError: nil,
},
{
name: "Valid Hex String Shorter than 16 bytes",
input: "0011223344556677",
expected: FixedString{
Data: [16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
},
expectedError: nil,
},
{
name: "Valid Hex String Longer than 16 bytes",
input: "00112233445566778899aabbccddeeffaabbccddeeff",
expected: FixedString{
Data: [16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
},
expectedError: nil,
},
{
name: "Invalid Hex String",
input: "invalidhexstring",
expected: FixedString{},
expectedError: fmt.Errorf("error decoding hex string: "),
},
{
name: "Empty Hex String",
input: "",
expected: FixedString{},
expectedError: fmt.Errorf("hex string is empty"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := NewFixedStringFromHex(test.input)
if test.expectedError != nil {
require.Error(t, err, "error should not be nil")
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
} else {
require.NoError(t, err, "converting hex to fixed string should not produce an error")
require.Equal(t, test.expected, result, "the result should match the expected value")
}
})
}
}
func TestFixedString_Hex(t *testing.T) {
tests := []struct {
name string
input FixedString
expected string
}{
{
name: "All Zeros",
input: FixedString{Data: [16]byte{}},
expected: "00000000000000000000000000000000",
},
{
name: "Mixed Data",
input: FixedString{Data: [16]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}},
expected: "000102030405060708090A0B0C0D0E0F",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := test.input.Hex()
require.Equal(t, test.expected, result)
})
}
}
func TestFixedString_MarshalBinary(t *testing.T) {
tests := []struct {
name string
input FixedString
expected []byte
}{
{
name: "All Zeros",
input: FixedString{Data: [16]byte{}},
expected: make([]byte, 16),
},
{
name: "Mixed Data",
input: FixedString{Data: [16]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}},
expected: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := test.input.MarshalBinary()
require.NoError(t, err)
require.Equal(t, test.expected, result)
})
}
}
func TestFixedString_UnmarshalBinary(t *testing.T) {
tests := []struct {
name string
input []byte
expected FixedString
}{
{
name: "All Zeros",
input: make([]byte, 16),
expected: FixedString{Data: [16]byte{}},
},
{
name: "Mixed Data",
input: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF},
expected: FixedString{Data: [16]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var result FixedString
err := result.UnmarshalBinary(test.input)
require.NoError(t, err)
require.Equal(t, test.expected, result)
})
}
}
func TestFixedString_Value(t *testing.T) {
tests := []struct {
name string
input FixedString
expected driver.Value
}{
{
name: "Default Value",
input: FixedString{},
expected: "",
},
{
name: "With Value",
input: FixedString{val: "example"},
expected: "example",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := test.input.Value()
require.NoError(t, err)
require.Equal(t, test.expected, *result.(*string))
})
}
}
func TestValidFQDN(t *testing.T) {
tests := []struct {
name string
value string
expected bool
}{
{
name: "Valid FQDN",
value: "example.com",
expected: true,
},
{
name: "Valid FQDN with Multiple Subdomains",
value: "sub.example.com",
expected: true,
},
{
name: "Valid FQDN with Hyphen",
value: "sub-domain.example.com",
expected: true,
},
{
name: "Single Label",
value: "example",
expected: false,
},
{
name: "Trailing Dot",
value: "example.com.",
expected: false,
},
{
name: "Invalid Underscore",
value: "sub_domain.example.com",
expected: false,
},
{
name: "Invalid Spaces",
value: "example .com",
expected: false,
},
{
name: "Invalid Special Characters",
value: "exa$mple.com",
expected: false,
},
{
name: "TLD Too Short",
value: "example.c",
expected: false,
},
{
name: "Empty String",
value: "",
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := ValidFQDN(test.value)
require.Equal(t, test.expected, result, "the result should match the expected value")
})
}
}
func TestContainsIP(t *testing.T) {
tests := []struct {
name string
subnets []Subnet
ip net.IP
contained bool
}{
{
name: "IP in subnet",
subnets: []Subnet{
{&net.IPNet{IP: net.IP{192, 168, 1, 0}.To16(), Mask: net.CIDRMask(24+96, 128)}},
},
ip: net.IP{192, 168, 1, 1},
contained: true,
},
{
name: "IP not in subnet",
subnets: []Subnet{
{&net.IPNet{IP: net.IP{192, 168, 1, 0}.To16(), Mask: net.CIDRMask(24+96, 128)}},
},
ip: net.IP{10, 0, 0, 1},
contained: false,
},
{
name: "IP in multiple subnets",
subnets: []Subnet{
{&net.IPNet{IP: net.IP{192, 168, 1, 0}.To16(), Mask: net.CIDRMask(24+96, 128)}},
{&net.IPNet{IP: net.IP{10, 0, 0, 0}.To16(), Mask: net.CIDRMask(8+96, 128)}},
},
ip: net.IP{10, 0, 0, 1},
contained: true,
},
{
name: "IPv6 address in subnet",
subnets: []Subnet{
{&net.IPNet{IP: net.ParseIP("2001:db8::"), Mask: net.CIDRMask(32, 128)}},
},
ip: net.ParseIP("2001:db8::1"),
contained: true,
},
{
name: "IPv6 address not in subnet",
subnets: []Subnet{
{&net.IPNet{IP: net.ParseIP("2001:db8::"), Mask: net.CIDRMask(32, 128)}},
},
ip: net.ParseIP("2001:db9::1"),
contained: false,
},
{
name: "Empty subnets list",
subnets: []Subnet{},
ip: net.IP{192, 168, 1, 1},
contained: false,
},
{
name: "IP in overlapping subnets",
subnets: []Subnet{
{&net.IPNet{IP: net.IP{192, 168, 0, 0}.To16(), Mask: net.CIDRMask(16+96, 128)}},
{&net.IPNet{IP: net.IP{192, 168, 1, 0}.To16(), Mask: net.CIDRMask(24+96, 128)}},
},
ip: net.IP{192, 168, 1, 1},
contained: true,
},
{
name: "IP in smaller overlapping subnet",
subnets: []Subnet{
{&net.IPNet{IP: net.IP{192, 168, 0, 0}.To16(), Mask: net.CIDRMask(16+96, 128)}},
{&net.IPNet{IP: net.IP{192, 168, 1, 0}.To16(), Mask: net.CIDRMask(28+96, 128)}},
},
ip: net.IP{192, 168, 1, 1},
contained: true,
},
{
name: "IP in IPv4 mapped IPv6 address",
subnets: NewTestSubnetList(t, []string{"::ffff:192.168.0.0/112", "::ffff:192.168.1.0/124"}),
ip: net.IP{192, 168, 1, 1},
contained: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := ContainsIP(test.subnets, test.ip)
require.Equal(t, test.contained, result, "contained should match expected value")
})
}
}
func TestIPIsPubliclyRoutable(t *testing.T) {
tests := []struct {
name string
ip net.IP
routable bool
}{
{
name: "Private IPv4 Address 1",
ip: net.IP{10, 0, 0, 1},
routable: false,
},
{
name: "Private IPv4 Address 2",
ip: net.IP{172, 16, 0, 1},
routable: false,
},
{
name: "Private IPv4 Address 3",
ip: net.IP{192, 168, 1, 1},
routable: false,
},
{
name: "Private IPv6 address",
ip: net.ParseIP("fc00::1"),
routable: false,
},
{
name: "Public IPv4 Address 1",
ip: net.IP{8, 8, 8, 8},
routable: true,
},
{
name: "Public IPv4 Address 2",
ip: net.IP{172, 217, 22, 14},
routable: true,
},
{
name: "Public IPv4 Address 3",
ip: net.IP{192, 0, 2, 0},
routable: true,
},
{
name: "Public IPv6 address",
ip: net.ParseIP("2001:4860:4860::8888"),
routable: true,
},
{
name: "Loopback IPv4 address",
ip: net.IP{127, 0, 0, 1},
routable: false,
},
{
name: "Link-local IPv4 Address",
ip: net.IP{169, 254, 0, 1},
routable: false,
},
{
name: "Loopback IPv6 address",
ip: net.ParseIP("::1"),
routable: false,
},
{
name: "Link-local IPv6 address",
ip: net.ParseIP("fe80::1"),
routable: false,
},
{
name: "Multicast IPv6 address",
ip: net.ParseIP("ff02::1"),
routable: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := IPIsPubliclyRoutable(test.ip)
require.Equal(t, test.routable, result, "routable should match expected value")
})
}
}
func TestParseNetworkID(t *testing.T) {
tests := []struct {
name string
ip net.IP
agentUUID string
expectedNetworkID uuid.UUID
}{
{ // agent uuids get overridden if ip is public
name: "Public IPv4 - Valid Agent UUID",
ip: net.IP{8, 8, 8, 8},
agentUUID: "02b1300b-dc4f-46dd-967e-698ccde5a920",
expectedNetworkID: PublicNetworkUUID,
},
{
name: "Public IPv4 - Invalid Agent UUID",
ip: net.IP{8, 8, 8, 8},
agentUUID: "bingbong",
expectedNetworkID: PublicNetworkUUID,
},
{
name: "Public IPv6 - Valid Agent UUID",
ip: net.ParseIP("2001:4860:4860::8888"),
agentUUID: "eb3a01e7-ac8a-461c-9582-b1d8727d240c",
expectedNetworkID: PublicNetworkUUID,
},
{
name: "Public IPv6 - Invalid Agent UUID",
ip: net.ParseIP("2001:4860:4860::8888"),
agentUUID: "bingbong",
expectedNetworkID: PublicNetworkUUID,
},
{
name: "Private IPv4 - Valid Agent UUID",
ip: net.IP{192, 168, 1, 1},
agentUUID: "a9f1052c-fea4-4209-8362-7f33d2630bf2",
},
{
name: "Private IPv4 with Invalid Agent UUID",
ip: net.IP{192, 168, 1, 1},
agentUUID: "invalid-bing-bong",
expectedNetworkID: UnknownPrivateNetworkUUID,
},
{
name: "Private IPv6 - Valid Agent UUID",
ip: net.ParseIP("fc00::1"),
agentUUID: "2b648d28-26a1-460f-b417-651192562258",
},
{
name: "Private IPv6 - Invalid Agent UUID",
ip: net.ParseIP("fc00::1"),
agentUUID: "invalid-bing-bong",
expectedNetworkID: UnknownPrivateNetworkUUID,
},
{
name: "Private IP with Empty Agent UUID",
ip: net.IP{192, 168, 1, 1},
agentUUID: "",
expectedNetworkID: UnknownPrivateNetworkUUID,
},
{
name: "Loopback IP",
ip: net.IP{127, 0, 0, 1},
agentUUID: "some-agent-id",
expectedNetworkID: UnknownPrivateNetworkUUID,
},
{
name: "Link-local IP",
ip: net.IP{169, 254, 0, 1},
agentUUID: "some-agent-id",
expectedNetworkID: UnknownPrivateNetworkUUID,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := ParseNetworkID(test.ip, test.agentUUID)
// if expected is not nil, check if the result matches the expected value
// otherwise, parse the expected value and vet against the result
if test.expectedNetworkID != uuid.Nil {
require.Equal(t, test.expectedNetworkID, result, "network uuid should match expected value")
} else {
uuid, err := uuid.Parse(test.agentUUID)
require.NoError(t, err)
require.Equal(t, uuid, result, "network uuid should match expected value")
}
})
}
}
func TestContainsDomain(t *testing.T) {
tests := []struct {
name string
host string
domains []string
contained bool
}{
{
name: "Exact Match",
domains: []string{"bingbong", "test.com"},
host: "bingbong",
contained: true,
},
{
name: "No Match",
domains: []string{"bingbong", "test.com"},
host: "notindomain.com",
contained: false,
},
{
name: "Wildcard Match",
domains: []string{"*.bingbong", "test.com"},
host: "sub.bingbong",
contained: true,
},
{
name: "Wildcard Top Domain",
domains: []string{"*.bingbong", "test.com"},
host: "bingbong",
contained: true,
},
{
name: "Wildcard Root",
domains: []string{"*.com"},
host: "bingbong.com",
contained: true,
},
{
name: "Wildcard, No Match",
domains: []string{"*.bingbong", "test.com"},
host: "sub.test.com",
contained: false,
},
{
name: "Multiple Wildcards, Match",
domains: []string{"*.bingbong", "*.test.com"},
host: "sub.test.com",
contained: true,
},
{
name: "Multiple Wildcards, No Match",
domains: []string{"*.bingbong", "*.test.com"},
host: "sub.another.com",
contained: false,
},
{
name: "Wildcard Match with Subdomain",
domains: []string{"*.bingbong", "test.com"},
host: "super.sub.bingbong",
contained: true,
},
{
name: "Empty Domains List",
domains: []string{},
host: "bingbong",
contained: false,
},
{
name: "Empty Host",
domains: []string{"bingbong", "test.com"},
host: "",
contained: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
contained := ContainsDomain(test.domains, test.host)
require.Equal(t, test.contained, contained, "contained should match expected value")
})
}
}
func TestSortUInt32s(t *testing.T) {
tests := []struct {
name string
data []uint32
expected []uint32
}{
{
name: "Already sorted",
data: []uint32{1, 2, 3, 4, 5},
expected: []uint32{1, 2, 3, 4, 5},
},
{
name: "Reverse order",
data: []uint32{5, 4, 3, 2, 1},
expected: []uint32{1, 2, 3, 4, 5},
},
{
name: "Unsorted",
data: []uint32{3, 1, 4, 5, 2},
expected: []uint32{1, 2, 3, 4, 5},
},
{
name: "With duplicates",
data: []uint32{3, 1, 4, 1, 5, 2, 3},
expected: []uint32{1, 1, 2, 3, 3, 4, 5},
},
{
name: "Single element",
data: []uint32{1},
expected: []uint32{1},
},
{
name: "Empty slice",
data: []uint32{},
expected: []uint32{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
SortUInt32s(test.data)
require.Equal(t, test.expected, test.data, "the sorted data should match the expected value")
})
}
}
func TestUInt32sAreSorted(t *testing.T) {
tests := []struct {
name string
data []uint32
expected bool
}{
{
name: "Sorted data",
data: []uint32{1, 2, 3, 4, 5},
expected: true,
},
{
name: "Unsorted data",
data: []uint32{5, 3, 4, 1, 2},
expected: false,
},
{
name: "Empty data",
data: []uint32{},
expected: true,
},
{
name: "Single element",
data: []uint32{42},
expected: true,
},
{
name: "All elements equal",
data: []uint32{7, 7, 7, 7},
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := UInt32sAreSorted(test.data)
require.Equal(t, test.expected, result, "the result should match the expected value")
})
}
}
func TestValidateTimestamp(t *testing.T) {
tests := []struct {
name string
timestamp time.Time
expectedTime time.Time
replaced bool
}{
{
name: "Valid timestamp",
timestamp: time.Date(2024, time.June, 3, 23, 24, 10, 0, time.Local),
expectedTime: time.Date(2024, time.June, 3, 23, 24, 10, 0, time.Local),
replaced: false,
},
{
name: "Log Floating-Pont Timestamp",
timestamp: time.Unix(1517336108, int64((0.231879)*1e9)), // 1517336108.231879
expectedTime: time.Unix(1517336108, 231879000),
replaced: false,
},
{
name: "Unset Timestamp",
timestamp: time.Time{},
expectedTime: time.Unix(0, 1),
replaced: true,
},
{
name: "MaxInt64 timestamp",
timestamp: time.Unix(math.MaxInt64, 0),
expectedTime: time.Unix(0, 1),
replaced: true,
},
{
name: "Negative timestamp",
timestamp: time.Unix(-1, 0),
expectedTime: time.Unix(0, 1),
replaced: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ts, replaced := ValidateTimestamp(test.timestamp)
require.Equal(t, test.expectedTime, ts, "timestamp should match expected value")
require.Equal(t, test.replaced, replaced, "replaced should match expected value")
})
}
}
func TestGetRelativeFirstSeenTimestamp(t *testing.T) {
t.Run("Use Current Time", func(t *testing.T) {
// make random max time
maxTime := time.Now().UTC().Add(-5 * time.Hour)
currentTime := time.Now()
ts := GetRelativeFirstSeenTimestamp(true, maxTime)
require.WithinDuration(t, currentTime, ts, time.Second)
require.NotEqual(t, maxTime, ts)
})
t.Run("Use Max Time", func(t *testing.T) {
maxTime := time.Now().UTC().Add(-5 * time.Hour)
ts := GetRelativeFirstSeenTimestamp(false, maxTime)
require.Equal(t, maxTime, ts)
})
}
func TestParseRelativePath(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)
workingDir, err := os.Getwd()
require.NoError(t, err)
currentDir := path.Dir(path.Join(workingDir))
tests := []struct {
name string
path string
mockGetUserHomeDir func() (string, error)
mockGetWorkingDir func() (string, error)
expected string
expectedErr error
}{
{
name: "Home directory",
path: "~/data",
expected: home + "/data",
},
{
name: "Current directory path",
path: "./",
expected: workingDir,
// expectedPath: filepath.Join(currentDir, "./mydir"),
},
{
name: "Relative directory - 1 deep",
path: "./data",
expected: workingDir + "/data",
},
{
name: "Relative directory - 2 deep",
path: "../data",
expected: currentDir + "/data",
},
{
name: "Absolute path",
path: "/home/logs",
expected: "/home/logs",
},
{
name: "Empty path",
expected: "",
expectedErr: ErrInvalidPath,
},
{
name: "Error Getting User Home Directory",
path: "~/data",
mockGetUserHomeDir: func() (string, error) {
return "", fmt.Errorf("forced get user home dir error")
},
expectedErr: fmt.Errorf("forced get user home dir error"),
},
{
name: "Error Getting Working Directory",
path: "./data",
mockGetWorkingDir: func() (string, error) {
return "", fmt.Errorf("forced get working dir error")
},
expectedErr: fmt.Errorf("forced get working dir error"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// restore the original functions after the test
origGetUserHomeDir := getUserHomeDir
origGetWorkingDir := getWorkingDir
defer func() { getUserHomeDir = origGetUserHomeDir; getWorkingDir = origGetWorkingDir }()
// mock functions if needed
if test.mockGetUserHomeDir != nil {
getUserHomeDir = test.mockGetUserHomeDir
}
if test.mockGetWorkingDir != nil {
getWorkingDir = test.mockGetWorkingDir
}
result, err := ParseRelativePath(test.path)
if test.expectedErr != nil {
require.EqualError(t, err, test.expectedErr.Error(), "error should match expected value")
} else {
require.NoError(t, err, "parsing relative path should not produce an error")
require.Equal(t, test.expected, result, "relative path should match expected value, got: %s, expected: %s", result, test.expected)
}
})
}
}
func TestValidateDirectory(t *testing.T) {
tests := []struct {
name string
setup func(afs afero.Fs)
dir string
mockPathExists func(fs afero.Fs, path string) (bool, error)
expectedError error
}{
{
name: "Directory is Valid",
setup: func(afs afero.Fs) {
require.NoError(t, afs.Mkdir("/nonemptydir", 0755))
require.NoError(t, afero.WriteFile(afs, "/nonemptydir/file.txt", []byte("content"), 0644))
},
dir: "/nonemptydir",
expectedError: nil,
},
{
name: "Directory Does Not Exist",
setup: func(_ afero.Fs) {},
dir: "/nonexistent",
expectedError: ErrDirDoesNotExist,
},
{
name: "Path is Not a Directory",
setup: func(afs afero.Fs) {
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte("content"), 0644))
},
dir: "/file.txt",
expectedError: ErrPathIsNotDir,
},
{
name: "Directory is Empty",
setup: func(afs afero.Fs) {
require.NoError(t, afs.Mkdir("/emptydir", 0755))
},
dir: "/emptydir",
expectedError: ErrDirIsEmpty,
},
{
name: "Validate Path Error",
setup: func(afs afero.Fs) {},
dir: "/some/path",
mockPathExists: func(fs afero.Fs, path string) (bool, error) {
return false, fmt.Errorf("forced existence check error")
},
expectedError: fmt.Errorf("forced existence check error"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// restore the original functions after the test
origPathExists := pathExists
defer func() { pathExists = origPathExists }()
// mock functions if needed
if test.mockPathExists != nil {
pathExists = test.mockPathExists
}
afs := afero.NewMemMapFs()
test.setup(afs)
err := ValidateDirectory(afs, test.dir)
if test.expectedError != nil {
require.Error(t, err, "error should not be nil")
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
} else {
require.NoError(t, err, "validating directory should not produce an error")
}
})
}
}
func TestValidateFile(t *testing.T) {
tests := []struct {
name string
setup func(afs afero.Fs)
file string
mockPathExists func(fs afero.Fs, path string) (bool, error)
expectedError error
}{
{
name: "File is Valid",
setup: func(afs afero.Fs) {
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte("content"), 0644))
},
file: "/file.txt",
},
{
name: "File is Empty",
setup: func(afs afero.Fs) {
require.NoError(t, afero.WriteFile(afs, "/emptyfile.txt", []byte(""), 0644))
},
file: "/emptyfile.txt",
expectedError: ErrFileIsEmtpy,
},
{
name: "File Does Not Exist",
setup: func(_ afero.Fs) {},
file: "/nonexistent",
expectedError: ErrFileDoesNotExist,
},
{
name: "Path is a Directory",
setup: func(afs afero.Fs) {
require.NoError(t, afs.Mkdir("/directory", 0755))
},
file: "/directory",
expectedError: ErrPathIsDir,
},
{
name: "Validate Path Error",
setup: func(afs afero.Fs) {},
file: "/some/path",
mockPathExists: func(fs afero.Fs, path string) (bool, error) {
return false, fmt.Errorf("forced existence check error")
},
expectedError: fmt.Errorf("forced existence check error"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// restore the original functions after the test
origPathExists := pathExists
defer func() { pathExists = origPathExists }()
// mock functions if needed
if test.mockPathExists != nil {
pathExists = test.mockPathExists
}
afs := afero.NewMemMapFs()
test.setup(afs)
err := ValidateFile(afs, test.file)
if test.expectedError != nil {
require.Error(t, err, "error should not be nil")
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
} else {
require.NoError(t, err, "validating file should not produce an error")
}
})
}
}
func TestValidatePath(t *testing.T) {
tests := []struct {
name string
setup func(afs afero.Fs)
path string
mockPathExists func(fs afero.Fs, path string) (bool, error)
mockIsDirectory func(fs afero.Fs, path string) (bool, error)
mockIsEmpty func(fs afero.Fs, path string) (bool, error)
expected [3]bool // exists, isDir, isEmpty
expectedError error
}{
{
name: "Path is Valid Non-Empty File",
setup: func(afs afero.Fs) {
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte("content"), 0644))
},
path: "/file.txt",
expected: [3]bool{true, false, false},
expectedError: nil,
},
{
name: "Path is Valid Empty File",
setup: func(afs afero.Fs) {
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte(""), 0644))
},
path: "/file.txt",
expected: [3]bool{true, false, true},
expectedError: nil,
},
{
name: "Path is Valid Non-Empty Directory",
setup: func(afs afero.Fs) {
require.NoError(t, afs.Mkdir("/nonemptydir", 0755))
require.NoError(t, afero.WriteFile(afs, "/nonemptydir/file.txt", []byte("content"), 0644))
},
path: "/nonemptydir",
expected: [3]bool{true, true, false},
expectedError: nil,
},
{
name: "Path is Valid Empty Directory",
setup: func(afs afero.Fs) {
require.NoError(t, afs.Mkdir("/emptydir", 0755))
},
path: "/emptydir",
expected: [3]bool{true, true, true},
expectedError: nil,
},
{
name: "Non-Existent Path",
setup: func(_ afero.Fs) {},
path: "/nonexistent",
expected: [3]bool{false, false, false},
expectedError: nil, // no error, just not found
},
{
name: "Empty Path",
setup: func(_ afero.Fs) {},
path: "",
expected: [3]bool{false, false, false},
expectedError: ErrInvalidPath,
},
{
name: "Nil filesystem",
setup: func(_ afero.Fs) {},
path: "/some/path",
expected: [3]bool{false, false, false},
expectedError: ErrFileSystemIsNil,
},
{
name: "Path Existece Check Error",
setup: func(_ afero.Fs) {},
path: "/some/path",
mockPathExists: func(fs afero.Fs, path string) (bool, error) { return false, fmt.Errorf("existence check forced error") },
expected: [3]bool{false, false, false},
expectedError: fmt.Errorf("existence check forced error"),
},
{
name: "Is Directory Check Error",
setup: func(afs afero.Fs) { require.NoError(t, afs.Mkdir("/emptydir", 0755)) },
path: "/emptydir",
mockIsDirectory: func(fs afero.Fs, path string) (bool, error) { return false, fmt.Errorf("isDir check forced error") },
expected: [3]bool{false, false, false},
expectedError: fmt.Errorf("isDir check forced error"),
},
{
name: "Is Empty Check Error",
setup: func(afs afero.Fs) { require.NoError(t, afs.Mkdir("/emptydir", 0755)) },
path: "/emptydir",
mockIsEmpty: func(fs afero.Fs, path string) (bool, error) { return false, fmt.Errorf("isEmpty check forced error") },
expected: [3]bool{true, true, false},
expectedError: fmt.Errorf("isEmpty check forced error"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// restore the original functions after the test
origPathExists := pathExists
origIsDirectory := isDirectory
origIsEmpty := isEmpty
defer func() { pathExists = origPathExists; isDirectory = origIsDirectory; isEmpty = origIsEmpty }()
// mock functions if needed
if test.mockPathExists != nil {
pathExists = test.mockPathExists
}
if test.mockIsDirectory != nil {
isDirectory = test.mockIsDirectory
}
if test.mockIsEmpty != nil {
isEmpty = test.mockIsEmpty
}
var afs afero.Fs
if test.name != "Nil filesystem" {
afs = afero.NewMemMapFs()
}
test.setup(afs)
exists, isDir, isEmpty, err := validatePath(afs, test.path)
if test.expectedError != nil {
require.Error(t, err)
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
} else {
require.NoError(t, err, "validating path should not produce an error")
require.Equal(t, test.expected[0], exists, "exist flag should be %v", test.expected[0])
require.Equal(t, test.expected[1], isDir, "isDir flag should be %v", test.expected[1])
require.Equal(t, test.expected[2], isEmpty, "isEmpty flag should be %v", test.expected[2])
}
})
}
}
func TestGetFileContents(t *testing.T) {
// define test cases
tests := []struct {
name string
path string
fileContents []byte
mockReadFile func(afero.Fs, string) ([]byte, error)
expectedError error
}{
{
name: "Valid Generated file",
path: "/valid/file/path",
fileContents: []byte("file contents"),
},
{
name: "Empty File",
path: "/invalid/file/path",
fileContents: []byte(""),
expectedError: ErrFileIsEmtpy,
},
{
name: "Invalid File Path",
path: "/missing/file/path",
expectedError: ErrFileDoesNotExist,
},
{
name: "Read File Error",
path: "/valid/file/path",
fileContents: []byte("file contents"),
mockReadFile: func(_ afero.Fs, _ string) ([]byte, error) {
return nil, fmt.Errorf("forced read file error")
},
expectedError: fmt.Errorf("forced read file error"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// restore the original function after the test
originalReadFileFunc := readFile
defer func() { readFile = originalReadFileFunc }()
// mock the readFile function
if test.mockReadFile != nil {
readFile = test.mockReadFile
}
// create a new memory filesystem
afs := afero.NewMemMapFs()
// create the file if the test case specifies contents
if test.fileContents != nil {
require.NoError(t, afero.WriteFile(afs, test.path, test.fileContents, 0644), "failed to create file")
}
// call readFile and check the results
result, err := GetFileContents(afs, test.path)
// validate results
if test.expectedError != nil {
require.Error(t, err, "expected an error but got none")
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
} else {
require.NoError(t, err, "did not expect an error but got one")
require.Equal(t, test.fileContents, result, "file contents should match expected value")
}
})
}
}
func TestCheckForNewerVersion(t *testing.T) {
tests := []struct {
name string
latestVersion string
currentVersion string
expectedNewer bool
expectedError error
}{
{
name: "Newer version available",
latestVersion: "v1.1.0",
currentVersion: "v1.0.0",
expectedNewer: true,
},
{
name: "No newer version",
latestVersion: "v1.0.0",
currentVersion: "v1.0.0",
expectedNewer: false,
},
{
name: "Invalid current version",
latestVersion: "v1.1.0",
currentVersion: "invalid-version",
expectedNewer: false,
expectedError: ErrParsingCurrentVersion,
},
{
name: "Invalid latest version",
latestVersion: "invalid-version",
currentVersion: "v1.0.0",
expectedNewer: false,
expectedError: ErrParsingLatestVersion,
},
{
name: "Error Fetching Latest Release",
expectedError: ErrFetchingLatestRelease,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if tt.expectedError == ErrFetchingLatestRelease {
http.Error(w, "error", http.StatusInternalServerError)
} else {
fmt.Fprintf(w, `{"tag_name": "%s"}`, tt.latestVersion)
}
}))
defer ts.Close()
// Override the GitHub client base URL
client := github.NewClient(nil)
newBaseURL, err := client.BaseURL.Parse(ts.URL + "/")
require.NoError(t, err, "failed to parse base URL")
client.BaseURL = newBaseURL
// Check for newer version
newer, version, err := CheckForNewerVersion(client, tt.currentVersion)
// Check for expected error
if tt.expectedError != nil {
require.Error(t, err, "error was expected")
require.ErrorContains(t, err, tt.expectedError.Error(), "error should contain expected value")
} else {
require.NoError(t, err, "checking for newer version should not produce an error")
// Check the expected values
require.Equal(t, tt.expectedNewer, newer)
require.Equal(t, tt.latestVersion, version)
}
})
}
}
func TestGetLatestReleaseVersion(t *testing.T) {
tests := []struct {
name string
owner string
repo string
latestVersion string
expected string
expectedError error
}{
{
name: "Valid Latest Release",
owner: "activecm",
repo: "rita",
latestVersion: "v2.0.0",
expected: "v2.0.0",
},
{
name: "Error Fetching Latest Release",
owner: "activecm",
repo: "rita",
expected: "",
expectedError: ErrFetchingLatestRelease,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Create a test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if test.expectedError != nil {
http.Error(w, "error", http.StatusInternalServerError)
} else {
fmt.Fprintf(w, `{"tag_name": "%s"}`, test.latestVersion)
}
}))
defer ts.Close()
// Override the GitHub client base URL
client := github.NewClient(nil)
newBaseURL, err := client.BaseURL.Parse(ts.URL + "/")
require.NoError(t, err, "failed to parse base URL")
client.BaseURL = newBaseURL
result, err := GetLatestReleaseVersion(client, test.owner, test.repo)
if test.expectedError != nil {
require.Error(t, err, "error should not be nil")
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
} else {
require.NoError(t, err, "fetching latest release should not produce an error")
require.Equal(t, test.expected, result, "the result should match the expected value")
}
})
}
}