Files
fscan/core/base_scan_strategy_test.go
ZacharyZcR 71b92d4408 feat: v2.1.0 核心重构与功能增强
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n

## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池

## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)

## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建

## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
  JSON 输出格式、Redis 检测、Context 超时等

## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
2026-01-11 20:16:23 +08:00

355 lines
8.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"testing"
)
// =============================================================================
// 插件列表解析测试
// =============================================================================
/*
插件列表解析 - parsePluginList 函数测试
测试价值:用户输入解析是扫描器的入口,解析错误会导致用户指定的插件无法执行
"字符串解析看起来简单,但边界情况会咬你一口。空格、空字符串、
逗号分隔符——这些是真实的bug来源。必须测试。"
*/
// TestParsePluginList_BasicCases 测试基本的插件列表解析
func TestParsePluginList_BasicCases(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{
name: "单个插件",
input: "ssh",
expected: []string{"ssh"},
},
{
name: "两个插件-逗号分隔",
input: "ssh,redis",
expected: []string{"ssh", "redis"},
},
{
name: "多个插件-逗号分隔",
input: "ssh,redis,mysql,mssql",
expected: []string{"ssh", "redis", "mysql", "mssql"},
},
{
name: "空字符串",
input: "",
expected: []string{},
},
{
name: "单个逗号",
input: ",",
expected: []string{},
},
{
name: "多个逗号",
input: ",,,",
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parsePluginList(tt.input)
if !slicesEqual(result, tt.expected) {
t.Errorf("parsePluginList(%q) = %v, want %v",
tt.input, result, tt.expected)
}
})
}
}
// TestParsePluginList_Whitespace 测试空格处理
func TestParsePluginList_Whitespace(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{
name: "插件名前后有空格",
input: " ssh ",
expected: []string{"ssh"},
},
{
name: "逗号前后有空格",
input: "ssh , redis",
expected: []string{"ssh", "redis"},
},
{
name: "多个空格",
input: " ssh , redis ",
expected: []string{"ssh", "redis"},
},
{
name: "Tab字符",
input: "ssh\t,\tredis",
expected: []string{"ssh", "redis"},
},
{
name: "混合空白字符",
input: " \tssh\t , \tredis \t",
expected: []string{"ssh", "redis"},
},
{
name: "只有空格",
input: " ",
expected: []string{},
},
{
name: "空格和逗号混合",
input: " , , , ",
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parsePluginList(tt.input)
if !slicesEqual(result, tt.expected) {
t.Errorf("parsePluginList(%q) = %v, want %v",
tt.input, result, tt.expected)
}
})
}
}
// TestParsePluginList_EdgeCases 测试边界情况
func TestParsePluginList_EdgeCases(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{
name: "连续逗号",
input: "ssh,,redis",
expected: []string{"ssh", "redis"},
},
{
name: "开头有逗号",
input: ",ssh,redis",
expected: []string{"ssh", "redis"},
},
{
name: "结尾有逗号",
input: "ssh,redis,",
expected: []string{"ssh", "redis"},
},
{
name: "开头结尾都有逗号",
input: ",ssh,redis,",
expected: []string{"ssh", "redis"},
},
{
name: "空元素混合",
input: "ssh, ,redis, , ,mysql",
expected: []string{"ssh", "redis", "mysql"},
},
{
name: "单字符插件名",
input: "a,b,c",
expected: []string{"a", "b", "c"},
},
{
name: "长插件名",
input: "verylongpluginname1,verylongpluginname2",
expected: []string{"verylongpluginname1", "verylongpluginname2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parsePluginList(tt.input)
if !slicesEqual(result, tt.expected) {
t.Errorf("parsePluginList(%q) = %v, want %v",
tt.input, result, tt.expected)
}
})
}
}
// TestParsePluginList_ProductionScenarios 测试生产环境真实场景
func TestParsePluginList_ProductionScenarios(t *testing.T) {
t.Run("用户复制粘贴带空格", func(t *testing.T) {
// 用户从文档复制 "ssh, redis, mysql" 粘贴到命令行
input := "ssh, redis, mysql"
expected := []string{"ssh", "redis", "mysql"}
result := parsePluginList(input)
if !slicesEqual(result, expected) {
t.Errorf("应该正确处理用户复制粘贴的空格")
}
})
t.Run("用户手误多打逗号", func(t *testing.T) {
// 用户打错了:"ssh,,redis"
input := "ssh,,redis"
expected := []string{"ssh", "redis"}
result := parsePluginList(input)
if !slicesEqual(result, expected) {
t.Errorf("应该容错处理连续逗号")
}
})
t.Run("常见的all模式", func(t *testing.T) {
// 虽然 "all" 在上层处理,但解析器也要能处理
input := "all"
expected := []string{"all"}
result := parsePluginList(input)
if !slicesEqual(result, expected) {
t.Errorf("应该正确解析 'all' 关键字")
}
})
t.Run("混合大小写插件名", func(t *testing.T) {
// Go插件名通常小写但用户可能输入大写
input := "SSH,Redis,MySQL"
expected := []string{"SSH", "Redis", "MySQL"}
result := parsePluginList(input)
// 注意:当前实现不做大小写转换,保留原始输入
if !slicesEqual(result, expected) {
t.Errorf("应该保留原始大小写(交给上层验证)")
}
})
}
// TestParsePluginList_ReturnValue 测试返回值特性
func TestParsePluginList_ReturnValue(t *testing.T) {
t.Run("返回空切片而非nil", func(t *testing.T) {
result := parsePluginList("")
if result == nil {
t.Error("空输入应该返回空切片而不是nil")
}
if len(result) != 0 {
t.Errorf("空输入应该返回长度为0的切片got length %d", len(result))
}
})
t.Run("返回新切片-不共享内存", func(t *testing.T) {
input := "ssh,redis"
result1 := parsePluginList(input)
result2 := parsePluginList(input)
// 修改result1不应该影响result2
if len(result1) > 0 {
result1[0] = "modified"
if result2[0] == "modified" {
t.Error("每次调用应该返回新的切片,不共享内存")
}
}
})
}
// slicesEqual 比较两个字符串切片是否相等
func slicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// TestNewBaseScanStrategy 测试构造函数
func TestNewBaseScanStrategy(t *testing.T) {
tests := []struct {
name string
strategyName string
filterType PluginFilterType
}{
{
name: "FilterNone",
strategyName: "无过滤",
filterType: FilterNone,
},
{
name: "FilterLocal",
strategyName: "本地扫描",
filterType: FilterLocal,
},
{
name: "FilterService",
strategyName: "服务扫描",
filterType: FilterService,
},
{
name: "FilterWeb",
strategyName: "Web扫描",
filterType: FilterWeb,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
strategy := NewBaseScanStrategy(tt.strategyName, tt.filterType)
if strategy == nil {
t.Fatal("NewBaseScanStrategy 返回 nil")
}
if strategy.strategyName != tt.strategyName {
t.Errorf("strategyName: 期望 %q, 实际 %q", tt.strategyName, strategy.strategyName)
}
if strategy.filterType != tt.filterType {
t.Errorf("filterType: 期望 %d, 实际 %d", tt.filterType, strategy.filterType)
}
})
}
}
// TestPluginFilterTypeConstants 测试过滤器类型常量
func TestPluginFilterTypeConstants(t *testing.T) {
// 验证常量值的唯一性和连续性
filterTypes := []PluginFilterType{
FilterNone,
FilterLocal,
FilterService,
FilterWeb,
}
// 检查值是否唯一
seen := make(map[PluginFilterType]bool)
for _, ft := range filterTypes {
if seen[ft] {
t.Errorf("PluginFilterType 值重复: %d", ft)
}
seen[ft] = true
}
// 验证预期值
expectedValues := map[PluginFilterType]int{
FilterNone: 0,
FilterLocal: 1,
FilterService: 2,
FilterWeb: 3,
}
for ft, expectedVal := range expectedValues {
if int(ft) != expectedVal {
t.Errorf("PluginFilterType %d: 期望值 %d, 实际值 %d", ft, expectedVal, int(ft))
}
}
}
// TestBaseScanStrategy_ValidateConfiguration 测试配置验证
func TestBaseScanStrategy_ValidateConfiguration(t *testing.T) {
strategy := NewBaseScanStrategy("测试", FilterNone)
err := strategy.ValidateConfiguration()
if err != nil {
t.Errorf("ValidateConfiguration 应返回 nil, 实际: %v", err)
}
}