Files

585 lines
16 KiB
Go
Raw Permalink Normal View History

package core
import (
2026-05-13 14:41:23 +08:00
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/shadow1ng/fscan/common"
2026-05-13 14:41:23 +08:00
"github.com/shadow1ng/fscan/plugins"
)
/*
scanner_test.go - Scanner核心逻辑测试
注意:scanner.go 包含大量副作用(HTTP初始化、信号处理、并发控制)。
本测试文件专注于可测试的纯逻辑和算法正确性:
1. 策略选择逻辑(selectStrategy) - 测试4种扫描模式的优先级
2. 端口解析逻辑(parsePort) - 测试端口范围验证(1-65535)
3. 任务计数逻辑验证(countApplicableTasks) - 使用mock策略测试
测试发现并修复的Bug:
- Bug #1: strconv.Atoi接受负数端口(如 "-80" 被解析为 -80)✅ 已修复
- Bug #2: strconv.Atoi不验证端口范围(如 "99999" 被解析为 99999,超出65535)✅ 已修复
修复方案:
在scanner.go中添加了 parsePort() 辅助函数,验证端口范围 (1-65535)。
非法端口会被记录到日志并返回0,避免传递给插件系统导致未定义行为。
"这代码需要依赖注入,不是测试。但既然现在无法重构,
我们至少验证策略选择和任务计数的逻辑是对的。
更重要的是,测试发现了两个真实的bug,并且都修复了。"
*/
// =============================================================================
// 核心逻辑测试:策略选择
// =============================================================================
2026-05-13 14:41:23 +08:00
func TestWebResultSerializerPreservesDetectedProtocol(t *testing.T) {
serializer := resultSerializers[plugins.ResultTypeWeb]
details := map[string]interface{}{}
result := &plugins.Result{
Type: plugins.ResultTypeWeb,
Success: true,
Output: "https://192.168.1.1:8443",
}
info := &common.HostInfo{Host: "192.168.1.1", Port: 8443}
serializer.fillDetail(result, info, details)
if details["protocol"] != "https" {
t.Fatalf("protocol = %v, 期望 https", details["protocol"])
}
if details["url"] != "https://192.168.1.1:8443" {
t.Fatalf("url = %v, 期望检测出的URL", details["url"])
}
}
// TestSelectStrategy 测试策略选择逻辑
func TestSelectStrategy(t *testing.T) {
// 保存原始配置
cfg := common.GetGlobalConfig()
state := common.GetGlobalState()
origAliveOnly := cfg.AliveOnly
origMode := cfg.Mode
origLocalMode := cfg.LocalMode
origURLs := state.GetURLs()
defer func() {
cfg.AliveOnly = origAliveOnly
cfg.Mode = origMode
cfg.LocalMode = origLocalMode
state.SetURLs(origURLs)
}()
tests := []struct {
name string
setupConfig func()
expectedType string
info common.HostInfo
}{
{
name: "存活检测模式-AliveOnly优先级最高",
setupConfig: func() {
cfg.AliveOnly = true
cfg.Mode = ""
cfg.LocalMode = false
state.SetURLs(nil)
},
expectedType: "*core.AliveScanStrategy",
info: common.HostInfo{Host: "192.168.1.1"},
},
{
name: "存活检测模式-ScanMode=icmp",
setupConfig: func() {
cfg.AliveOnly = false
cfg.Mode = "icmp"
cfg.LocalMode = false
state.SetURLs(nil)
},
expectedType: "*core.AliveScanStrategy",
info: common.HostInfo{Host: "192.168.1.1"},
},
{
name: "本地模式-LocalMode",
setupConfig: func() {
cfg.AliveOnly = false
cfg.Mode = ""
cfg.LocalMode = true
state.SetURLs(nil)
},
expectedType: "*core.LocalScanStrategy",
info: common.HostInfo{Host: "localhost"},
},
{
name: "Web扫描模式-URLs非空",
setupConfig: func() {
cfg.AliveOnly = false
cfg.Mode = ""
cfg.LocalMode = false
state.SetURLs([]string{"http://example.com"})
},
expectedType: "*core.WebScanStrategy",
info: common.HostInfo{Host: "example.com"},
},
{
name: "服务扫描模式-默认",
setupConfig: func() {
cfg.AliveOnly = false
cfg.Mode = ""
cfg.LocalMode = false
state.SetURLs(nil)
},
expectedType: "*core.ServiceScanStrategy",
info: common.HostInfo{Host: "192.168.1.1", Port: 22},
},
{
name: "优先级测试-AliveOnly覆盖LocalMode",
setupConfig: func() {
cfg.AliveOnly = true
cfg.Mode = ""
cfg.LocalMode = true // 被AliveOnly覆盖
state.SetURLs(nil)
},
expectedType: "*core.AliveScanStrategy",
info: common.HostInfo{Host: "localhost"},
},
{
name: "优先级测试-LocalMode覆盖URLs",
setupConfig: func() {
cfg.AliveOnly = false
cfg.Mode = ""
cfg.LocalMode = true
state.SetURLs([]string{"http://example.com"}) // 被LocalMode覆盖
},
expectedType: "*core.LocalScanStrategy",
info: common.HostInfo{Host: "localhost"},
},
{
name: "优先级测试-URLs覆盖默认服务扫描",
setupConfig: func() {
cfg.AliveOnly = false
cfg.Mode = ""
cfg.LocalMode = false
state.SetURLs([]string{"http://example.com"})
},
expectedType: "*core.WebScanStrategy",
info: common.HostInfo{Host: "192.168.1.1", Port: 80},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 设置配置
tt.setupConfig()
// 执行策略选择
strategy := selectStrategy(cfg, state, tt.info)
// 验证策略类型
strategyType := fmt.Sprintf("%T", strategy)
if strategyType != tt.expectedType {
t.Errorf("selectStrategy() 类型 = %s, 期望 %s", strategyType, tt.expectedType)
}
// 验证策略不为nil
if strategy == nil {
t.Error("selectStrategy() 返回 nil")
}
})
}
}
// TestSelectStrategy_AllModesDisabled 测试所有模式禁用时的默认行为
func TestSelectStrategy_AllModesDisabled(t *testing.T) {
// 保存原始配置
cfg := common.GetGlobalConfig()
state := common.GetGlobalState()
origAliveOnly := cfg.AliveOnly
origMode := cfg.Mode
origLocalMode := cfg.LocalMode
origURLs := state.GetURLs()
defer func() {
cfg.AliveOnly = origAliveOnly
cfg.Mode = origMode
cfg.LocalMode = origLocalMode
state.SetURLs(origURLs)
}()
// 设置所有模式为禁用状态
cfg.AliveOnly = false
cfg.Mode = ""
cfg.LocalMode = false
state.SetURLs(nil)
info := common.HostInfo{Host: "192.168.1.1"}
strategy := selectStrategy(cfg, state, info)
// 应该返回默认的ServiceScanStrategy
expectedType := "*core.ServiceScanStrategy"
strategyType := fmt.Sprintf("%T", strategy)
if strategyType != expectedType {
t.Errorf("默认策略类型 = %s, 期望 %s", strategyType, expectedType)
}
}
// =============================================================================
// =============================================================================
// 任务计数逻辑测试(需要mock策略)
// =============================================================================
// mockStrategy 用于测试的mock策略
type mockStrategy struct {
plugins []string
isCustomMode bool
applicablePlugins map[string]bool // pluginName -> isApplicable
}
2026-05-13 14:41:23 +08:00
func (m *mockStrategy) Execute(_ context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
}
func (m *mockStrategy) GetPlugins() ([]string, bool) {
return m.plugins, m.isCustomMode
}
func (m *mockStrategy) IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool {
if m.applicablePlugins == nil {
return true // 默认都适用
}
return m.applicablePlugins[pluginName]
}
// TestCountApplicableTasks 测试任务计数逻辑
func TestCountApplicableTasks(t *testing.T) {
tests := []struct {
name string
targets []common.HostInfo
strategy *mockStrategy
setupPlugins func()
expected int
}{
{
name: "空目标列表",
targets: []common.HostInfo{},
strategy: &mockStrategy{
plugins: []string{"ssh", "mysql"},
isCustomMode: false,
},
setupPlugins: func() {},
expected: 0,
},
{
name: "单目标单插件",
targets: []common.HostInfo{
{Host: "192.168.1.1", Port: 22},
},
strategy: &mockStrategy{
plugins: []string{"ssh"},
isCustomMode: false,
},
setupPlugins: func() {},
expected: 1, // 取决于插件是否存在
},
{
name: "单目标多插件",
targets: []common.HostInfo{
{Host: "192.168.1.1", Port: 22},
},
strategy: &mockStrategy{
plugins: []string{"ssh", "mysql", "redis"},
isCustomMode: false,
},
setupPlugins: func() {},
expected: 3, // 假设所有插件都存在且适用
},
{
name: "多目标单插件",
targets: []common.HostInfo{
{Host: "192.168.1.1", Port: 22},
{Host: "192.168.1.2", Port: 22},
{Host: "192.168.1.3", Port: 22},
},
strategy: &mockStrategy{
plugins: []string{"ssh"},
isCustomMode: false,
},
setupPlugins: func() {},
expected: 3,
},
{
name: "多目标多插件",
targets: []common.HostInfo{
{Host: "192.168.1.1", Port: 22},
{Host: "192.168.1.2", Port: 80},
},
strategy: &mockStrategy{
plugins: []string{"ssh", "http"},
isCustomMode: false,
},
setupPlugins: func() {},
expected: 4, // 2 targets * 2 plugins
},
{
name: "部分插件不适用",
targets: []common.HostInfo{
{Host: "192.168.1.1", Port: 22},
{Host: "192.168.1.2", Port: 80},
},
strategy: &mockStrategy{
plugins: []string{"ssh", "http", "mysql"},
isCustomMode: false,
applicablePlugins: map[string]bool{
"ssh": true,
"http": true,
"mysql": false, // mysql不适用
},
},
setupPlugins: func() {},
expected: 4, // 2 targets * 2 applicable plugins
},
{
name: "空端口-端口为0",
targets: []common.HostInfo{
{Host: "192.168.1.1", Port: 0},
},
strategy: &mockStrategy{
plugins: []string{"ssh"},
isCustomMode: false,
},
setupPlugins: func() {},
expected: 1,
},
{
name: "非法端口-解析为0",
targets: []common.HostInfo{
{Host: "192.168.1.1", Port: 0},
},
strategy: &mockStrategy{
plugins: []string{"ssh"},
isCustomMode: false,
},
setupPlugins: func() {},
expected: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupPlugins()
// 注意:实际的countApplicableTasks依赖plugins.Exists()
// 这里我们只能测试逻辑结构,无法验证实际插件系统
// 这是"上帝函数"的典型问题:无法mock依赖
// 提取纯逻辑测试
count := 0
for _, target := range tt.targets {
targetPort := target.Port
pluginsToRun, isCustomMode := tt.strategy.GetPlugins()
for _, pluginName := range pluginsToRun {
// 跳过plugins.Exists检查(无法mock)
if tt.strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
count++
}
}
}
if count != tt.expected {
t.Errorf("任务计数 = %d, 期望 %d", count, tt.expected)
}
})
}
}
// =============================================================================
// 边界情况测试
// =============================================================================
// TestSelectStrategy_EmptyHostInfo 测试空HostInfo的策略选择
func TestSelectStrategy_EmptyHostInfo(t *testing.T) {
// 保存原始配置
cfg := common.GetGlobalConfig()
state := common.GetGlobalState()
origAliveOnly := cfg.AliveOnly
origMode := cfg.Mode
origLocalMode := cfg.LocalMode
origURLs := state.GetURLs()
defer func() {
cfg.AliveOnly = origAliveOnly
cfg.Mode = origMode
cfg.LocalMode = origLocalMode
state.SetURLs(origURLs)
}()
cfg.AliveOnly = false
cfg.Mode = ""
cfg.LocalMode = false
state.SetURLs(nil)
emptyInfo := common.HostInfo{}
strategy := selectStrategy(cfg, state, emptyInfo)
if strategy == nil {
t.Error("selectStrategy() 不应对空HostInfo返回nil")
}
// 应该返回默认策略
expectedType := "*core.ServiceScanStrategy"
strategyType := fmt.Sprintf("%T", strategy)
if strategyType != expectedType {
t.Errorf("空HostInfo策略类型 = %s, 期望 %s", strategyType, expectedType)
}
}
// =============================================================================
// buildScanReport 测试
// =============================================================================
// TestBuildScanReport 验证 buildScanReport 字段映射正确
func TestBuildScanReport(t *testing.T) {
state := common.NewState()
// 填充各计数器
state.SetEnd(10)
state.SetNum(7)
state.IncrementTCPSuccessPacketCount() // +1 total, +1 tcp, +1 tcpSuccess
state.IncrementTCPSuccessPacketCount() // +1 total, +1 tcp, +1 tcpSuccess
state.IncrementTCPFailedPacketCount() // +1 total, +1 tcp, +1 tcpFailed
state.IncrementUDPPacketCount() // +1 total, +1 udp
state.IncrementHTTPPacketCount() // +1 total, +1 http
state.IncrementResourceExhaustedCount()
start := time.Now().Add(-time.Second) // 模拟 1 秒前开始
report := buildScanReport(state, start)
if report.TasksTotal != 10 {
t.Errorf("TasksTotal = %d, 期望 10", report.TasksTotal)
}
if report.TasksCompleted != 7 {
t.Errorf("TasksCompleted = %d, 期望 7", report.TasksCompleted)
}
if report.Packets != 5 {
t.Errorf("Packets = %d, 期望 5", report.Packets)
}
if report.TCPPackets != 3 {
t.Errorf("TCPPackets = %d, 期望 3", report.TCPPackets)
}
if report.TCPSuccessPackets != 2 {
t.Errorf("TCPSuccessPackets = %d, 期望 2", report.TCPSuccessPackets)
}
if report.TCPFailedPackets != 1 {
t.Errorf("TCPFailedPackets = %d, 期望 1", report.TCPFailedPackets)
}
if report.UDPPackets != 1 {
t.Errorf("UDPPackets = %d, 期望 1", report.UDPPackets)
}
if report.HTTPPackets != 1 {
t.Errorf("HTTPPackets = %d, 期望 1", report.HTTPPackets)
}
if report.ResourceExhausted != 1 {
t.Errorf("ResourceExhausted = %d, 期望 1", report.ResourceExhausted)
}
if report.Duration < time.Millisecond {
t.Errorf("Duration = %v, 期望 >= 1ms", report.Duration)
}
}
// TestBuildScanReport_ZeroState 验证空 State 返回零值报告
func TestBuildScanReport_ZeroState(t *testing.T) {
state := common.NewState()
start := time.Now()
report := buildScanReport(state, start)
if report.TasksTotal != 0 || report.TasksCompleted != 0 || report.Packets != 0 {
t.Errorf("空 State 期望全零报告,实际 %+v", report)
}
if report.Duration < 0 {
t.Errorf("Duration 不能为负: %v", report.Duration)
}
}
// =============================================================================
// determineScanMode IsLocalMode 分支测试
// =============================================================================
// TestDetermineScanMode_IsLocalModeCallback 覆盖 IsLocalMode 回调分支
func TestDetermineScanMode_IsLocalModeCallback(t *testing.T) {
// 保存原始值
origIsLocalMode := common.IsLocalMode
defer func() { common.IsLocalMode = origIsLocalMode }()
// 注册回调:mode == "localtest" 时认为是本地模式
common.IsLocalMode = func(mode string) bool {
return mode == "localtest"
}
cfg := &common.Config{
AliveOnly: false,
Mode: "localtest",
LocalMode: false,
}
state := common.NewState()
mode := determineScanMode(cfg, state)
if mode != ScanModeLocal {
t.Errorf("determineScanMode() = %v, 期望 ScanModeLocal", mode)
}
// 回调命中后应同时设置 LocalMode 和 LocalPlugin
if !cfg.LocalMode {
t.Error("IsLocalMode 命中后应设置 cfg.LocalMode = true")
}
if cfg.LocalPlugin != "localtest" {
t.Errorf("LocalPlugin = %q, 期望 \"localtest\"", cfg.LocalPlugin)
}
}
// TestDetermineScanMode_IsLocalModeCallbackNoMatch 回调不命中时不影响模式
func TestDetermineScanMode_IsLocalModeCallbackNoMatch(t *testing.T) {
origIsLocalMode := common.IsLocalMode
defer func() { common.IsLocalMode = origIsLocalMode }()
common.IsLocalMode = func(mode string) bool { return false }
cfg := &common.Config{
AliveOnly: false,
Mode: "something",
LocalMode: false,
}
state := common.NewState()
mode := determineScanMode(cfg, state)
if mode != ScanModeService {
t.Errorf("回调不命中时期望 ScanModeService, 实际 %v", mode)
}
}
// TestCountApplicableTasks_EmptyPlugins 测试空插件列表
func TestCountApplicableTasks_EmptyPlugins(t *testing.T) {
targets := []common.HostInfo{
{Host: "192.168.1.1", Port: 22},
}
strategy := &mockStrategy{
plugins: []string{},
isCustomMode: false,
}
count := 0
for _, target := range targets {
targetPort := target.Port
pluginsToRun, isCustomMode := strategy.GetPlugins()
for _, pluginName := range pluginsToRun {
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
count++
}
}
}
if count != 0 {
t.Errorf("空插件列表应返回0任务, 实际 %d", count)
}
}