2026-04-25 17:39:16 +08:00
|
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-13 14:41:23 +08:00
|
|
|
|
"context"
|
2026-04-25 17:39:16 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"testing"
|
2026-06-15 19:11:15 +08:00
|
|
|
|
"time"
|
2026-04-25 17:39:16 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/shadow1ng/fscan/common"
|
2026-05-13 14:41:23 +08:00
|
|
|
|
"github.com/shadow1ng/fscan/plugins"
|
2026-04-25 17:39:16 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
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"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 17:39:16 +08:00
|
|
|
|
// 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) {
|
2026-04-25 17:39:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 19:11:15 +08:00
|
|
|
|
// =============================================================================
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-25 17:39:16 +08:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|