xfy d4998e5634 feat(ssl,security): 实现 SSL/TLS 和安全中间件模块
- ssl: TLS 配置管理、证书加载、SNI 支持、现代安全默认值
- security/auth: HTTP Basic Auth (bcrypt/argon2id 密码哈希)
- security/ratelimit: 令牌桶限流、连接数限制
- security/access: IP 访问控制 (CIDR allow/deny)
- security/headers: 安全响应头 (X-Frame-Options, CSP, HSTS 等)

Phase 4 完成

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-03 09:53:18 +08:00

343 lines
6.7 KiB
Go

package security
import (
"net"
"testing"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
func TestNewAccessControl(t *testing.T) {
tests := []struct {
name string
cfg *config.AccessConfig
wantErr bool
}{
{
name: "nil config",
cfg: nil,
wantErr: true,
},
{
name: "empty config",
cfg: &config.AccessConfig{},
},
{
name: "valid allow list",
cfg: &config.AccessConfig{
Allow: []string{"192.168.1.0/24", "10.0.0.1"},
},
},
{
name: "valid deny list",
cfg: &config.AccessConfig{
Deny: []string{"192.168.2.100/32"},
},
},
{
name: "invalid CIDR",
cfg: &config.AccessConfig{
Allow: []string{"invalid"},
},
wantErr: true,
},
{
name: "default allow",
cfg: &config.AccessConfig{
Default: "allow",
},
},
{
name: "default deny",
cfg: &config.AccessConfig{
Default: "deny",
},
},
{
name: "invalid default",
cfg: &config.AccessConfig{
Default: "invalid",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac, err := NewAccessControl(tt.cfg)
if (err != nil) != tt.wantErr {
t.Errorf("NewAccessControl() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && ac == nil {
t.Error("Expected non-nil AccessControl")
}
})
}
}
func TestAccessControlCheck(t *testing.T) {
tests := []struct {
name string
cfg *config.AccessConfig
ip string
expected bool
}{
{
name: "default allow",
cfg: &config.AccessConfig{
Default: "allow",
},
ip: "192.168.1.100",
expected: true,
},
{
name: "default deny",
cfg: &config.AccessConfig{
Default: "deny",
},
ip: "192.168.1.100",
expected: false,
},
{
name: "explicit allow",
cfg: &config.AccessConfig{
Allow: []string{"192.168.1.0/24"},
Default: "deny",
},
ip: "192.168.1.100",
expected: true,
},
{
name: "not in allow list",
cfg: &config.AccessConfig{
Allow: []string{"192.168.1.0/24"},
Default: "deny",
},
ip: "192.168.2.100",
expected: false,
},
{
name: "explicit deny",
cfg: &config.AccessConfig{
Deny: []string{"192.168.2.100"},
Default: "allow",
},
ip: "192.168.2.100",
expected: false,
},
{
name: "deny takes precedence",
cfg: &config.AccessConfig{
Allow: []string{"192.168.0.0/16"},
Deny: []string{"192.168.2.100"},
Default: "deny",
},
ip: "192.168.2.100",
expected: false,
},
{
name: "single IP allow",
cfg: &config.AccessConfig{
Allow: []string{"10.0.0.1"},
Default: "deny",
},
ip: "10.0.0.1",
expected: true,
},
{
name: "IPv6 allow",
cfg: &config.AccessConfig{
Allow: []string{"2001:db8::/32"},
Default: "deny",
},
ip: "2001:db8::1",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac, err := NewAccessControl(tt.cfg)
if err != nil {
t.Fatalf("NewAccessControl() error: %v", err)
}
ip := net.ParseIP(tt.ip)
if ip == nil {
t.Fatalf("Invalid IP: %s", tt.ip)
}
result := ac.Check(ip)
if result != tt.expected {
t.Errorf("Check(%s) = %v, expected %v", tt.ip, result, tt.expected)
}
})
}
}
func TestAccessControlProcess(t *testing.T) {
ac, err := NewAccessControl(&config.AccessConfig{
Allow: []string{"127.0.0.1"},
Default: "deny",
})
if err != nil {
t.Fatalf("NewAccessControl() error: %v", err)
}
// Create a simple handler
nextHandler := func(ctx *fasthttp.RequestCtx) {
ctx.WriteString("OK")
}
handler := ac.Process(nextHandler)
// Verify the handler is created correctly
if handler == nil {
t.Error("Process() returned nil handler")
}
}
func TestParseCIDR(t *testing.T) {
tests := []struct {
name string
cidr string
wantErr bool
}{
{
name: "valid IPv4 CIDR",
cidr: "192.168.1.0/24",
},
{
name: "valid IPv4 single",
cidr: "192.168.1.1",
},
{
name: "valid IPv6 CIDR",
cidr: "2001:db8::/32",
},
{
name: "valid IPv6 single",
cidr: "2001:db8::1",
},
{
name: "invalid IP",
cidr: "invalid",
wantErr: true,
},
{
name: "invalid CIDR",
cidr: "192.168.1.0/33",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
network, err := parseCIDR(tt.cidr)
if (err != nil) != tt.wantErr {
t.Errorf("parseCIDR() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && network == nil {
t.Error("Expected non-nil network")
}
})
}
}
func TestUpdateAllowList(t *testing.T) {
ac, err := NewAccessControl(&config.AccessConfig{
Default: "deny",
})
if err != nil {
t.Fatalf("NewAccessControl() error: %v", err)
}
// Update allow list
err = ac.UpdateAllowList([]string{"10.0.0.0/8"})
if err != nil {
t.Errorf("UpdateAllowList() error: %v", err)
}
// Check that IP is now allowed
ip := net.ParseIP("10.0.0.1")
if !ac.Check(ip) {
t.Error("Expected IP to be allowed after update")
}
// Test invalid update
err = ac.UpdateAllowList([]string{"invalid"})
if err == nil {
t.Error("Expected error for invalid CIDR")
}
}
func TestUpdateDenyList(t *testing.T) {
ac, err := NewAccessControl(&config.AccessConfig{
Allow: []string{"0.0.0.0/0"},
Default: "allow",
})
if err != nil {
t.Fatalf("NewAccessControl() error: %v", err)
}
// Update deny list
err = ac.UpdateDenyList([]string{"192.168.2.0/24"})
if err != nil {
t.Errorf("UpdateDenyList() error: %v", err)
}
// Check that IP is now denied
ip := net.ParseIP("192.168.2.1")
if ac.Check(ip) {
t.Error("Expected IP to be denied after update")
}
}
func TestSetDefault(t *testing.T) {
ac, err := NewAccessControl(&config.AccessConfig{
Default: "allow",
})
if err != nil {
t.Fatalf("NewAccessControl() error: %v", err)
}
// Change to deny
err = ac.SetDefault("deny")
if err != nil {
t.Errorf("SetDefault() error: %v", err)
}
stats := ac.GetStats()
if stats.Default != "deny" {
t.Errorf("Expected default 'deny', got %s", stats.Default)
}
// Test invalid action
err = ac.SetDefault("invalid")
if err == nil {
t.Error("Expected error for invalid action")
}
}
func TestGetStats(t *testing.T) {
ac, err := NewAccessControl(&config.AccessConfig{
Allow: []string{"192.168.1.0/24", "10.0.0.0/8"},
Deny: []string{"192.168.2.100"},
Default: "deny",
})
if err != nil {
t.Fatalf("NewAccessControl() error: %v", err)
}
stats := ac.GetStats()
if stats.AllowCount != 2 {
t.Errorf("Expected AllowCount 2, got %d", stats.AllowCount)
}
if stats.DenyCount != 1 {
t.Errorf("Expected DenyCount 1, got %d", stats.DenyCount)
}
if stats.Default != "deny" {
t.Errorf("Expected Default 'deny', got %s", stats.Default)
}
}