diff --git a/internal/utils/bytes_test.go b/internal/utils/bytes_test.go new file mode 100644 index 0000000..6f184ea --- /dev/null +++ b/internal/utils/bytes_test.go @@ -0,0 +1,95 @@ +// Package utils 提供字节操作工具函数的测试。 +// +// 该文件测试 B2s 和 S2b 函数,包括: +// - 空值处理 +// - 正常值转换 +// - 内存共享验证 +// +// 作者:xfy +package utils + +import ( + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +func TestB2s(t *testing.T) { + tests := []struct { + name string + input []byte + want string + }{ + {"nil_slice", nil, ""}, + {"empty_slice", []byte{}, ""}, + {"single_byte", []byte("a"), "a"}, + {"ascii_string", []byte("hello world"), "hello world"}, + {"utf8_string", []byte("你好世界"), "你好世界"}, + {"special_chars", []byte("!@#$%^&*()"), "!@#$%^&*()"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := B2s(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestB2s_ZeroAlloc(t *testing.T) { + original := []byte("test") + s := B2s(original) + ptr := unsafe.StringData(s) + slicePtr := unsafe.SliceData(original) + assert.Equal(t, slicePtr, ptr, "B2s result should share memory with original slice") +} + +func TestS2b(t *testing.T) { + tests := []struct { + name string + input string + want []byte + }{ + {"empty_string", "", nil}, + {"single_char", "a", []byte("a")}, + {"ascii_string", "hello world", []byte("hello world")}, + {"utf8_string", "你好世界", []byte("你好世界")}, + {"special_chars", "!@#$%^&*()", []byte("!@#$%^&*()")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := S2b(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestS2b_ZeroAlloc(t *testing.T) { + original := "test" + b := S2b(original) + strPtr := unsafe.StringData(original) + slicePtr := unsafe.SliceData(b) + assert.Equal(t, strPtr, slicePtr, "S2b result should share memory with original string") +} + +func TestB2s_S2b_RoundTrip(t *testing.T) { + tests := []struct { + name string + value string + }{ + {"empty", ""}, + {"ascii", "hello"}, + {"utf8", "你好"}, + {"binary", "\x00\x01\xff"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := S2b(tt.value) + s := B2s(b) + assert.Equal(t, tt.value, s) + }) + } +} diff --git a/internal/utils/etag_test.go b/internal/utils/etag_test.go new file mode 100644 index 0000000..08b7f39 --- /dev/null +++ b/internal/utils/etag_test.go @@ -0,0 +1,86 @@ +// Package utils 提供 ETag 生成工具函数的测试。 +// +// 该文件测试 GenerateETag 函数,包括: +// - 正常参数生成 +// - 零值参数 +// - 大数值参数 +// - 格式验证 +// +// 作者:xfy +package utils + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeETag(unix int64, size int64) string { + return fmt.Sprintf(`"%x-%x"`, unix, size) +} + +func TestGenerateETag(t *testing.T) { + tests := []struct { + name string + modTime time.Time + size int64 + }{ + {"valid_time_and_size", time.Unix(1609459200, 0), 1024}, + {"zero_time", time.Time{}, 100}, + {"zero_size", time.Unix(1609459200, 0), 0}, + {"large_size", time.Unix(1609459200, 0), 1<<62 - 1}, + {"negative_size", time.Unix(1609459200, 0), -1}, + {"negative_modtime", time.Unix(-1000, 0), 500}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GenerateETag(tt.modTime, tt.size) + want := makeETag(tt.modTime.Unix(), tt.size) + assert.Equal(t, want, got) + }) + } +} + +func TestGenerateETag_Format(t *testing.T) { + etag := GenerateETag(time.Unix(1609459200, 0), 1024) + + assert.True(t, strings.HasPrefix(etag, "\""), "ETag should start with a quote") + assert.True(t, strings.HasSuffix(etag, "\""), "ETag should end with a quote") + assert.Equal(t, byte('"'), etag[0]) + assert.Equal(t, byte('"'), etag[len(etag)-1]) + + inner := strings.Trim(etag, "\"") + parts := strings.SplitN(inner, "-", 2) + require.Len(t, parts, 2, "inner ETag should have exactly one hyphen separator") + + for _, part := range parts { + for _, c := range part { + assert.True(t, (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || c == '-', + "character %c in ETag part should be hex digit or minus sign", c) + } + } +} + +func TestGenerateETag_Deterministic(t *testing.T) { + modTime := time.Unix(1700000000, 0) + size := int64(2048) + + etag1 := GenerateETag(modTime, size) + etag2 := GenerateETag(modTime, size) + assert.Equal(t, etag1, etag2, "same inputs should produce identical ETags") +} + +func TestGenerateETag_DifferentInputs(t *testing.T) { + t1 := time.Unix(1700000000, 0) + t2 := time.Unix(1700000001, 0) + + assert.NotEqual(t, GenerateETag(t1, 100), GenerateETag(t2, 100), + "different modtimes should produce different ETags") + assert.NotEqual(t, GenerateETag(t1, 100), GenerateETag(t1, 200), + "different sizes should produce different ETags") +} diff --git a/internal/utils/ipallowlist_test.go b/internal/utils/ipallowlist_test.go new file mode 100644 index 0000000..9364b9c --- /dev/null +++ b/internal/utils/ipallowlist_test.go @@ -0,0 +1,179 @@ +// Package utils 提供 IP 白名单工具函数的测试。 +// +// 该文件测试 ParseIPAllowList、ParseCIDR、IPInAllowList 函数,包括: +// - 空列表处理 +// - CIDR 格式解析 +// - 单 IP 自动转换 +// - localhost 特殊值 +// - 无效输入处理 +// - IP 匹配检查 +// +// 作者:xfy +package utils + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseIPAllowList(t *testing.T) { + tests := []struct { + name string + input []string + wantLen int + wantErr bool + }{ + {"nil_input", nil, 0, false}, + {"empty_input", []string{}, 0, false}, + {"single_cidr_v4", []string{"192.168.1.0/24"}, 1, false}, + {"single_cidr_v6", []string{"::1/128"}, 1, false}, + {"single_ipv4", []string{"192.168.1.1"}, 1, false}, + {"single_ipv6", []string{"::1"}, 1, false}, + {"localhost_expansion", []string{"localhost"}, 2, false}, + {"multiple_entries", []string{"192.168.1.0/24", "10.0.0.1", "::1/128"}, 3, false}, + {"localhost_with_others", []string{"localhost", "10.0.0.0/8"}, 3, false}, + {"invalid_entry", []string{"not-an-ip"}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseIPAllowList(tt.input) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Len(t, result, tt.wantLen) + }) + } +} + +func TestParseIPAllowList_Localhost(t *testing.T) { + result, err := ParseIPAllowList([]string{"localhost"}) + require.NoError(t, err) + require.Len(t, result, 2) + + assert.Equal(t, "127.0.0.1/32", result[0].String()) + assert.Equal(t, "::1/128", result[1].String()) +} + +func TestParseIPAllowList_SingleIPv4(t *testing.T) { + result, err := ParseIPAllowList([]string{"192.168.1.100"}) + require.NoError(t, err) + require.Len(t, result, 1) + assert.Equal(t, "192.168.1.100/32", result[0].String()) +} + +func TestParseIPAllowList_SingleIPv6(t *testing.T) { + result, err := ParseIPAllowList([]string{"2001:db8::1"}) + require.NoError(t, err) + require.Len(t, result, 1) + assert.Equal(t, "2001:db8::1/128", result[0].String()) +} + +func TestParseCIDR(t *testing.T) { + tests := []struct { + name string + input string + want string + wantIP string + }{ + {"cidr_v4", "192.168.1.0/24", "192.168.1.0/24", "192.168.1.0"}, + {"cidr_v6", "::1/128", "::1/128", "::1"}, + {"single_ipv4", "10.0.0.1", "10.0.0.1/32", "10.0.0.1"}, + {"single_ipv6", "::1", "::1/128", "::1"}, + {"slash16", "172.16.0.0/16", "172.16.0.0/16", "172.16.0.0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + network, err := ParseCIDR(tt.input) + require.NoError(t, err) + assert.Equal(t, tt.want, network.String()) + assert.Equal(t, tt.wantIP, network.IP.String()) + }) + } +} + +func TestParseCIDR_Invalid(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"empty_string", ""}, + {"invalid_ip", "not-an-ip"}, + {"invalid_cidr", "192.168.1.0/33"}, + {"invalid_cidr_v6", "::1/129"}, + {"garbage", "hello/world"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ParseCIDR(tt.input) + assert.Error(t, err) + }) + } +} + +func TestIPInAllowList(t *testing.T) { + _, v4Net, _ := net.ParseCIDR("192.168.1.0/24") + _, v6Net, _ := net.ParseCIDR("::1/128") + _, net10, _ := net.ParseCIDR("10.0.0.0/8") + + allowList := []net.IPNet{*v4Net, *v6Net, *net10} + + tests := []struct { + name string + ip string + wantMatch bool + }{ + {"matching_v4_in_range", "192.168.1.100", true}, + {"matching_v4_boundary_start", "192.168.1.0", true}, + {"matching_v4_boundary_end", "192.168.1.255", true}, + {"matching_v6_localhost", "::1", true}, + {"matching_10_range", "10.50.0.1", true}, + {"non_matching_v4", "172.16.0.1", false}, + {"non_matching_v6", "2001:db8::1", false}, + {"non_matching_nearby", "192.168.2.1", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ip := net.ParseIP(tt.ip) + require.NotNil(t, ip) + result := IPInAllowList(ip, allowList) + assert.Equal(t, tt.wantMatch, result) + }) + } +} + +func TestIPInAllowList_EmptyList(t *testing.T) { + ip := net.ParseIP("192.168.1.1") + require.NotNil(t, ip) + assert.False(t, IPInAllowList(ip, nil)) + assert.False(t, IPInAllowList(ip, []net.IPNet{})) +} + +func TestIPInAllowList_SingleEntry(t *testing.T) { + _, network, err := net.ParseCIDR("127.0.0.1/32") + require.NoError(t, err) + allowList := []net.IPNet{*network} + + assert.True(t, IPInAllowList(net.ParseIP("127.0.0.1"), allowList)) + assert.False(t, IPInAllowList(net.ParseIP("127.0.0.2"), allowList)) +} + +func TestParseIPAllowList_Integration(t *testing.T) { + result, err := ParseIPAllowList([]string{"192.168.1.0/24", "10.0.0.1", "localhost"}) + require.NoError(t, err) + require.Len(t, result, 4) + + assert.True(t, IPInAllowList(net.ParseIP("192.168.1.50"), result)) + assert.True(t, IPInAllowList(net.ParseIP("10.0.0.1"), result)) + assert.True(t, IPInAllowList(net.ParseIP("127.0.0.1"), result)) + assert.True(t, IPInAllowList(net.ParseIP("::1"), result)) + assert.False(t, IPInAllowList(net.ParseIP("8.8.8.8"), result)) +}