mirror of
https://github.com/oneclickvirt/backtrace.git
synced 2026-09-03 13:15:52 +08:00
215 lines
7.8 KiB
Go
215 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/oneclickvirt/backtrace/bgptools"
|
|
backtrace "github.com/oneclickvirt/backtrace/bk"
|
|
"github.com/oneclickvirt/backtrace/utils"
|
|
"github.com/oneclickvirt/basics/network/resolver"
|
|
)
|
|
|
|
func TestBacktraceStructuredFlagParsing(t *testing.T) {
|
|
var options cliOptions
|
|
set := newBacktraceFlagSet(&options)
|
|
if err := set.Parse([]string{"-structured", "-deep", "-ip", "192.0.2.1", "-timeout", "900ms"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !options.jsonOutput || !options.deep || options.specifiedIP != "192.0.2.1" || options.timeout != 900*time.Millisecond {
|
|
t.Fatalf("unexpected options: %+v", options)
|
|
}
|
|
}
|
|
|
|
func TestBacktraceDefaultsPreserveLegacyMode(t *testing.T) {
|
|
var options cliOptions
|
|
if err := newBacktraceFlagSet(&options).Parse(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !options.showIPInfo || options.jsonOutput || options.routeJSON || options.deep || options.ipv6 || options.specifiedIP != "" || options.dnsMode != "auto" || options.timeout != 15*time.Second || options.routeTries != 3 {
|
|
t.Fatalf("legacy defaults changed: %+v", options)
|
|
}
|
|
}
|
|
|
|
func TestBacktraceDNSModeParsing(t *testing.T) {
|
|
var options cliOptions
|
|
if err := newBacktraceFlagSet(&options).Parse([]string{"-dns-mode", "DoT"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
options.dnsMode = strings.ToLower(strings.TrimSpace(options.dnsMode))
|
|
if options.dnsMode != "dot" {
|
|
t.Fatalf("dns mode = %q, want dot", options.dnsMode)
|
|
}
|
|
}
|
|
|
|
func TestConfigureBacktraceResolverScopesBootstrapFallback(t *testing.T) {
|
|
originalConfigure := backtraceDNSConfigureFn
|
|
originalShutdown := backtraceDNSShutdownFn
|
|
originalBootstrapReachable := backtraceDNSBootstrapReachableFn
|
|
t.Cleanup(func() {
|
|
backtraceDNSConfigureFn = originalConfigure
|
|
backtraceDNSShutdownFn = originalShutdown
|
|
backtraceDNSBootstrapReachableFn = originalBootstrapReachable
|
|
})
|
|
tests := []struct {
|
|
name string
|
|
mode resolver.Mode
|
|
connected bool
|
|
bootstrapReachable bool
|
|
configuredStatus resolver.Status
|
|
wantConfigure int
|
|
wantBootstrap int
|
|
wantShutdown int
|
|
}{
|
|
{
|
|
name: "connected auto uses normal resolver path",
|
|
mode: resolver.ModeAuto,
|
|
connected: true,
|
|
configuredStatus: resolver.Status{Requested: resolver.ModeAuto, Active: resolver.ModeSystem, SystemAvailable: true},
|
|
wantConfigure: 1,
|
|
},
|
|
{
|
|
name: "offline auto stops when bootstrap is unreachable",
|
|
mode: resolver.ModeAuto,
|
|
wantBootstrap: 1,
|
|
wantShutdown: 1,
|
|
},
|
|
{
|
|
name: "offline auto retries after reachable bootstrap",
|
|
mode: resolver.ModeAuto,
|
|
bootstrapReachable: true,
|
|
configuredStatus: resolver.Status{Requested: resolver.ModeAuto, Active: resolver.ModeDoH, DoHAvailable: true, Fallback: true, Stack: "IPv4"},
|
|
wantConfigure: 1,
|
|
wantBootstrap: 1,
|
|
},
|
|
{
|
|
name: "forced DoH bypasses bootstrap gate",
|
|
mode: resolver.ModeDoH,
|
|
configuredStatus: resolver.Status{Requested: resolver.ModeDoH, Active: resolver.ModeDoH, DoHAvailable: true},
|
|
wantConfigure: 1,
|
|
},
|
|
{
|
|
name: "system mode never falls back",
|
|
mode: resolver.ModeSystem,
|
|
wantShutdown: 1,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
configureCalls := 0
|
|
bootstrapCalls := 0
|
|
shutdownCalls := 0
|
|
backtraceDNSConfigureFn = func(_ context.Context, config resolver.Config) resolver.Status {
|
|
configureCalls++
|
|
if config.Mode != test.mode {
|
|
t.Fatalf("resolver mode = %q, want %q", config.Mode, test.mode)
|
|
}
|
|
return test.configuredStatus
|
|
}
|
|
backtraceDNSBootstrapReachableFn = func(_ context.Context, config resolver.Config) (string, bool) {
|
|
bootstrapCalls++
|
|
if config.Mode != resolver.ModeAuto {
|
|
t.Fatalf("bootstrap mode = %q, want auto", config.Mode)
|
|
}
|
|
return "IPv4", test.bootstrapReachable
|
|
}
|
|
backtraceDNSShutdownFn = func() { shutdownCalls++ }
|
|
|
|
status := configureBacktraceResolver(context.Background(), test.mode, &utils.NetCheckResult{Connected: test.connected})
|
|
if configureCalls != test.wantConfigure || bootstrapCalls != test.wantBootstrap || shutdownCalls != test.wantShutdown {
|
|
t.Fatalf("DNS calls = configure:%d bootstrap:%d shutdown:%d, want configure:%d bootstrap:%d shutdown:%d", configureCalls, bootstrapCalls, shutdownCalls, test.wantConfigure, test.wantBootstrap, test.wantShutdown)
|
|
}
|
|
if test.wantConfigure == 0 && status.Active != resolver.ModeUnavailable {
|
|
t.Fatalf("status = %#v, want unavailable", status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPromoteEncryptedDNSConnectivity(t *testing.T) {
|
|
preCheck := &utils.NetCheckResult{StackType: "None"}
|
|
promoteEncryptedDNSConnectivity(preCheck, resolver.Status{Active: resolver.ModeDoT, Stack: "IPv6"})
|
|
if !preCheck.Connected || !preCheck.HasIPv6 || preCheck.HasIPv4 || preCheck.StackType != "IPv6" {
|
|
t.Fatalf("successful encrypted DNS did not promote network state: %#v", preCheck)
|
|
}
|
|
}
|
|
|
|
func TestBacktraceRouteStructuredFlagParsing(t *testing.T) {
|
|
var options cliOptions
|
|
set := newBacktraceFlagSet(&options)
|
|
if err := set.Parse([]string{"-route-json", "-ipv6", "-route-attempts", "4", "-timeout", "9s"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !options.routeJSON || !options.ipv6 || options.routeTries != 4 || options.timeout != 9*time.Second {
|
|
t.Fatalf("unexpected route options: %+v", options)
|
|
}
|
|
if err := validateStructuredOptions(options); err != nil {
|
|
t.Fatalf("valid route options rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateRouteStructuredOptions(t *testing.T) {
|
|
for _, options := range []cliOptions{
|
|
{jsonOutput: true, routeJSON: true, timeout: time.Second, routeTries: 3},
|
|
{routeJSON: true, timeout: 0, routeTries: 3},
|
|
{routeJSON: true, timeout: time.Second, routeTries: 0},
|
|
{routeJSON: true, timeout: time.Second, routeTries: 6},
|
|
} {
|
|
if err := validateStructuredOptions(options); err == nil {
|
|
t.Fatalf("expected invalid route options: %+v", options)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteStructuredRouteReportKeepsStdoutJSONOnly(t *testing.T) {
|
|
var output bytes.Buffer
|
|
report := backtrace.RouteReport{SchemaVersion: backtrace.RouteReportSchema, Targets: []backtrace.RouteTargetReport{}}
|
|
if err := writeStructuredRouteReport(&output, report); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var decoded backtrace.RouteReport
|
|
if err := json.Unmarshal(output.Bytes(), &decoded); err != nil {
|
|
t.Fatalf("stdout is not route JSON: %v (%q)", err, output.String())
|
|
}
|
|
if decoded.SchemaVersion != backtrace.RouteReportSchema {
|
|
t.Fatalf("unexpected route report: %+v", decoded)
|
|
}
|
|
}
|
|
|
|
func TestValidateStructuredOptionsRequiresExplicitModeAndTarget(t *testing.T) {
|
|
tests := []cliOptions{
|
|
{deep: true, timeout: time.Second},
|
|
{jsonOutput: true, timeout: time.Second},
|
|
{jsonOutput: true, specifiedIP: "192.0.2.1"},
|
|
}
|
|
for _, options := range tests {
|
|
if err := validateStructuredOptions(options); err == nil {
|
|
t.Fatalf("expected invalid structured options: %+v", options)
|
|
}
|
|
}
|
|
if err := validateStructuredOptions(cliOptions{jsonOutput: true, deep: true, specifiedIP: "192.0.2.1", timeout: time.Second}); err != nil {
|
|
t.Fatalf("valid structured options rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteStructuredReportKeepsStdoutJSONOnly(t *testing.T) {
|
|
var output bytes.Buffer
|
|
err := writeStructuredReport(context.Background(), &output, "192.0.2.1", bgptools.IPBGPReportConfig{Timeout: time.Second}, func(_ context.Context, ip string, _ bgptools.IPBGPReportConfig) (*bgptools.IPBGPReport, error) {
|
|
return &bgptools.IPBGPReport{IP: ip, Status: bgptools.ReportAvailable, Sources: []bgptools.IPBGPSourceStatus{}}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var report bgptools.IPBGPReport
|
|
if err := json.Unmarshal(output.Bytes(), &report); err != nil {
|
|
t.Fatalf("stdout is not JSON: %v (%q)", err, output.String())
|
|
}
|
|
if report.IP != "192.0.2.1" {
|
|
t.Fatalf("unexpected report: %+v", report)
|
|
}
|
|
}
|