Files
fscan/common/config_builder_test.go
T
ZacharyZcR 353d525642
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
test: 补充单元测试覆盖率 29.9% → 36.6%
新建 18 个测试文件,追加 30 个已有测试文件,覆盖协议解析、
错误分类、CEL 表达式求值、YAML 反序列化、字节编码等纯函数。
2026-06-15 19:11:15 +08:00

451 lines
13 KiB
Go
Raw 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 common
import (
"reflect"
"testing"
"time"
fscanconfig "github.com/shadow1ng/fscan/common/config"
)
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
fv := &FlagVars{
Password: "root admin,pass0",
AddPasswords: "pass1 pass2,pass3\tpass4",
}
got, err := parsePasswords(fv)
if err != nil {
t.Fatalf("parsePasswords error = %v", err)
}
// -pwd 逗号分隔,空格保留;-pwda 逗号/空格/tab 分隔
want := []string{"root admin", "pass0", "pass1", "pass2", "pass3", "pass4"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
}
}
func TestBuildConfigReturnsUserFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{UsersFile: "missing-users-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing users file")
}
}
func TestBuildConfigReturnsPasswordFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{PasswordsFile: "missing-passwords-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing passwords file")
}
}
func TestBuildConfigReturnsURLFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{URLsFile: "missing-urls-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing urls file")
}
}
func TestBuildConfigRejectsInvalidHashValue(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{HashValue: "not-md5"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for invalid hash value")
}
}
func TestBuildConfigDefaultsAreIndependentCopies(t *testing.T) {
cfg, _, err := BuildConfig(&FlagVars{Username: "custom-user"}, &HostInfo{})
if err != nil {
t.Fatalf("BuildConfig error = %v", err)
}
defaultSSHUsers := fscanconfig.DefaultUserDict["ssh"]
if len(defaultSSHUsers) == 1 && defaultSSHUsers[0] == "custom-user" {
t.Fatal("BuildConfig mutated DefaultUserDict")
}
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
t.Fatal("Config userdict shares backing storage with DefaultUserDict")
}
cfg.Credentials.Passwords[0] = "mutated-password"
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
t.Fatal("Config passwords share backing storage with DefaultPasswords")
}
port := 80
cfg.PortMap[port][0] = "mutated-probe"
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
t.Fatal("Config port map shares backing storage with DefaultPortMap")
}
cfg.DefaultMap[0] = "mutated-default-probe"
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
t.Fatal("Config default map shares backing storage with DefaultProbeMap")
}
}
func TestParseUserPassPairsKeepsAdditionalCredentialFlags(t *testing.T) {
tests := []struct {
name string
fv *FlagVars
}{
{
name: "additional passwords",
fv: &FlagVars{
Username: "root",
Password: "primary",
AddPasswords: "extra",
},
},
{
name: "additional users",
fv: &FlagVars{
Username: "root",
Password: "primary",
AddUsers: "admin",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pairs, err := parseUserPassPairs(tt.fv)
if err != nil {
t.Fatalf("parseUserPassPairs error = %v", err)
}
if len(pairs) != 0 {
t.Fatalf("parseUserPassPairs returned exact pairs %#v; additional credential flags would be ignored", pairs)
}
})
}
}
func TestNewConfigDefaultsAreIndependentCopies(t *testing.T) {
cfg := NewConfig()
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
t.Fatal("NewConfig userdict shares backing storage with DefaultUserDict")
}
cfg.Credentials.Passwords[0] = "mutated-password"
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
t.Fatal("NewConfig passwords share backing storage with DefaultPasswords")
}
port := 80
cfg.PortMap[port][0] = "mutated-probe"
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
t.Fatal("NewConfig port map shares backing storage with DefaultPortMap")
}
cfg.DefaultMap[0] = "mutated-default-probe"
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
t.Fatal("NewConfig default map shares backing storage with DefaultProbeMap")
}
}
func TestParseTargetsHostPortDoesNotLeaveSyntheticHost(t *testing.T) {
fv := &FlagVars{Ports: "22"}
info := &HostInfo{Host: "127.0.0.1:8080"}
cfg := BuildConfigFromFlags(fv)
state := NewState()
if err := parseTargets(fv, info, cfg, state); err != nil {
t.Fatalf("parseTargets error = %v", err)
}
if info.Host != "" {
t.Fatalf("info.Host = %q, want empty after host:port extraction", info.Host)
}
if got := state.GetHostPorts(); !reflect.DeepEqual(got, []string{"127.0.0.1:8080"}) {
t.Fatalf("hostPorts = %#v, want host:port target", got)
}
}
func TestNormalizeURLKeepsUppercaseScheme(t *testing.T) {
got := normalizeURL("HTTPS://example.com")
if got != "HTTPS://example.com" {
t.Fatalf("normalizeURL() = %q", got)
}
}
func TestNormalizeURLBracketsIPv6Literals(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "bare ipv6 without scheme", in: "2001:db8::1", want: "http://[2001:db8::1]"},
{name: "bracketed ipv6 without scheme", in: "[2001:db8::1]", want: "http://[2001:db8::1]"},
{name: "bare ipv6 with scheme", in: "http://2001:db8::1", want: "http://[2001:db8::1]"},
{name: "bare ipv6 path without scheme", in: "2001:db8::1/admin", want: "http://[2001:db8::1]/admin"},
{name: "bare ipv6 query without scheme", in: "2001:db8::1?debug=1", want: "http://[2001:db8::1]?debug=1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeURL(tt.in); got != tt.want {
t.Fatalf("normalizeURL(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
// TestModuleTimeout 测试模块超时计算
func TestModuleTimeout(t *testing.T) {
tests := []struct {
name string
timeout time.Duration
want time.Duration
}{
{"超时大于下限", 10 * time.Second, 10 * time.Second},
{"超时等于下限", 3 * time.Second, 3 * time.Second},
{"超时小于下限", 1 * time.Second, 3 * time.Second},
{"零超时", 0, 3 * time.Second},
{"负超时", -1 * time.Second, 3 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
cfg.Timeout = tt.timeout
got := cfg.ModuleTimeout()
if got != tt.want {
t.Errorf("ModuleTimeout() = %v, want %v", got, tt.want)
}
})
}
}
// TestParseUserPassPairsExactMatch 测试精确单用户单密码路径
func TestParseUserPassPairsExactMatch(t *testing.T) {
fv := &FlagVars{
Username: "admin",
Password: "secret",
}
pairs, err := parseUserPassPairs(fv)
if err != nil {
t.Fatalf("parseUserPassPairs error = %v", err)
}
if len(pairs) != 1 {
t.Fatalf("期望 1 个 pair, 实际 %d", len(pairs))
}
if pairs[0].Username != "admin" || pairs[0].Password != "secret" {
t.Errorf("pair = %+v, want {admin secret}", pairs[0])
}
}
// TestParseUserPassPairsMultiUserSkips 测试多用户时不生成精确 pair
func TestParseUserPassPairsMultiUserSkips(t *testing.T) {
fv := &FlagVars{
Username: "admin,root",
Password: "pass",
}
pairs, err := parseUserPassPairs(fv)
if err != nil {
t.Fatalf("parseUserPassPairs error = %v", err)
}
if len(pairs) != 0 {
t.Fatalf("多用户场景不应生成精确 pair, 实际 %d 个", len(pairs))
}
}
// TestParseURLsEmpty 测试空输入返回空列表
func TestParseURLsEmpty(t *testing.T) {
fv := &FlagVars{}
urls, err := parseURLs(fv)
if err != nil {
t.Fatalf("parseURLs error = %v", err)
}
if len(urls) != 0 {
t.Fatalf("空输入应返回空 url 列表, 实际 %v", urls)
}
}
// TestParseURLsCommaSeparated 测试逗号分隔多 URL
func TestParseURLsCommaSeparated(t *testing.T) {
fv := &FlagVars{
TargetURL: "http://a.com,http://b.com,http://a.com", // 含重复
}
urls, err := parseURLs(fv)
if err != nil {
t.Fatalf("parseURLs error = %v", err)
}
if len(urls) != 2 {
t.Fatalf("去重后应有 2 个 url, 实际 %d: %v", len(urls), urls)
}
}
// TestParseURLsMissingFile 测试缺失文件返回错误
func TestParseURLsMissingFile(t *testing.T) {
fv := &FlagVars{URLsFile: "nonexistent-urls.txt"}
_, err := parseURLs(fv)
if err == nil {
t.Fatal("缺失文件应返回错误")
}
}
// ---------------------------------------------------------------------------
// parseHashes
// ---------------------------------------------------------------------------
// TestParseHashesEmpty 空输入返回空结果
func TestParseHashesEmpty(t *testing.T) {
fv := &FlagVars{}
vals, bytes, err := parseHashes(fv)
if err != nil {
t.Fatalf("parseHashes error = %v", err)
}
if len(vals) != 0 || len(bytes) != 0 {
t.Fatalf("空输入应返回空结果, vals=%v bytes=%v", vals, bytes)
}
}
// TestParseHashesValidNTLM 纯 32 字符 hex hash
func TestParseHashesValidNTLM(t *testing.T) {
hash := "aabbccddeeff00112233445566778899"
fv := &FlagVars{HashValue: hash}
vals, hashBytes, err := parseHashes(fv)
if err != nil {
t.Fatalf("parseHashes error = %v", err)
}
if len(vals) != 1 || vals[0] != hash {
t.Fatalf("vals = %v, want [%s]", vals, hash)
}
if len(hashBytes) != 1 || len(hashBytes[0]) != 16 {
t.Fatalf("hashBytes length wrong: %v", hashBytes)
}
}
// TestParseHashesLMNTFormat LM:NT 格式,提取 NT 部分
func TestParseHashesLMNTFormat(t *testing.T) {
lm := "aad3b435b51404eeaad3b435b51404ee"
nt := "31d6cfe0d16ae931b73c59d7e0c089c0"
fv := &FlagVars{HashValue: lm + ":" + nt}
vals, _, err := parseHashes(fv)
if err != nil {
t.Fatalf("parseHashes error = %v", err)
}
if len(vals) != 1 || vals[0] != nt {
t.Fatalf("vals = %v, want [%s]", vals, nt)
}
}
// TestParseHashesInvalidLength hash 长度不是 32 → error
func TestParseHashesInvalidLength(t *testing.T) {
fv := &FlagVars{HashValue: "tooshort"}
_, _, err := parseHashes(fv)
if err == nil {
t.Fatal("hash 长度不足应返回错误")
}
}
// TestParseHashesInvalidHex 32 字符但含非 hex 字符 → error
func TestParseHashesInvalidHex(t *testing.T) {
fv := &FlagVars{HashValue: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}
_, _, err := parseHashes(fv)
if err == nil {
t.Fatal("非 hex 字符应返回错误")
}
}
// TestParseHashesMissingFile hash 文件不存在 → error
func TestParseHashesMissingFile(t *testing.T) {
fv := &FlagVars{HashFile: "nonexistent-hashes.txt"}
_, _, err := parseHashes(fv)
if err == nil {
t.Fatal("缺失 hash 文件应返回错误")
}
}
// ---------------------------------------------------------------------------
// parseUsernames
// ---------------------------------------------------------------------------
// TestParseUsernamesEmpty 空输入返回空结果
func TestParseUsernamesEmpty(t *testing.T) {
fv := &FlagVars{}
got, err := parseUsernames(fv)
if err != nil {
t.Fatalf("parseUsernames error = %v", err)
}
if len(got) != 0 {
t.Fatalf("空输入应返回空, got %v", got)
}
}
// TestParseUsernamesCommaSeparated 逗号分隔多用户
func TestParseUsernamesCommaSeparated(t *testing.T) {
fv := &FlagVars{Username: "admin, root, admin"} // 含重复和空格
got, err := parseUsernames(fv)
if err != nil {
t.Fatalf("parseUsernames error = %v", err)
}
want := []string{"admin", "root"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
// TestParseUsernamesAddUsers AddUsers 追加去重
func TestParseUsernamesAddUsers(t *testing.T) {
fv := &FlagVars{
Username: "admin",
AddUsers: "root,admin", // admin 重复
}
got, err := parseUsernames(fv)
if err != nil {
t.Fatalf("parseUsernames error = %v", err)
}
want := []string{"admin", "root"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
// TestParseUsernamesMissingFile 缺失用户文件 → error
func TestParseUsernamesMissingFile(t *testing.T) {
fv := &FlagVars{UsersFile: "nonexistent-users.txt"}
_, err := parseUsernames(fv)
if err == nil {
t.Fatal("缺失用户文件应返回错误")
}
}
// ---------------------------------------------------------------------------
// cloneStringSlice
// ---------------------------------------------------------------------------
// TestCloneStringSliceNil nil 输入返回 nil
func TestCloneStringSliceNil(t *testing.T) {
got := cloneStringSlice(nil)
if got != nil {
t.Fatalf("nil 输入应返回 nil, got %v", got)
}
}
// TestCloneStringSliceEmpty 空切片:append 无元素结果为 nillen 为 0
func TestCloneStringSliceEmpty(t *testing.T) {
got := cloneStringSlice([]string{})
if len(got) != 0 {
t.Fatalf("got len %d, want 0", len(got))
}
}
// TestCloneStringSliceCopiesValues 正常切片:值正确且独立
func TestCloneStringSliceCopiesValues(t *testing.T) {
src := []string{"a", "b", "c"}
got := cloneStringSlice(src)
if !reflect.DeepEqual(got, src) {
t.Fatalf("got %v, want %v", got, src)
}
// 修改 clone 不影响原始
got[0] = "mutated"
if src[0] != "a" {
t.Fatal("cloneStringSlice 返回的切片与源共享底层数组")
}
}