Files
chen08209 2fa2f7f816 feat(core): bind the Android Core with gomobile
Changelog: Share link import and mrs rule sets
2026-10-11 12:28:52 +08:00

331 lines
10 KiB
Go

package core
import (
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"strconv"
"testing"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/tunnel"
)
func TestDefaultSetupParams(t *testing.T) {
params := defaultSetupParams()
if params.TestURL != "https://www.gstatic.com/generate_204" {
t.Errorf("TestURL = %s, want the gstatic generate_204 probe", params.TestURL)
}
if params.SelectedMap == nil {
t.Error("SelectedMap must be a usable map so decoding can merge into it")
}
}
func TestDefaultSetupParamsSurvivesPartialDecode(t *testing.T) {
params := defaultSetupParams()
if err := json.Unmarshal([]byte(`{"selected-map":{"GLOBAL":"auto"}}`), params); err != nil {
t.Fatalf("decode error: %v", err)
}
if params.TestURL != "https://www.gstatic.com/generate_204" {
t.Errorf("TestURL = %s, want the default to survive a partial payload", params.TestURL)
}
if params.SelectedMap["GLOBAL"] != "auto" {
t.Errorf("SelectedMap = %v, want GLOBAL mapped to auto", params.SelectedMap)
}
}
func TestSetupParamsDecodeStartsFromTheDefaults(t *testing.T) {
var absent, empty SetupParams
if err := json.Unmarshal([]byte(`{"skip-cert-verify":true}`), &absent); err != nil {
t.Fatalf("decode error: %v", err)
}
if err := json.Unmarshal([]byte(`{"test-url":""}`), &empty); err != nil {
t.Fatalf("decode error: %v", err)
}
if absent.TestURL != defaultTestURL || absent.SelectedMap == nil || !absent.SkipCertVerify {
t.Errorf("params = %+v, want the defaults under the decoded fields", absent)
}
if empty.TestURL != "" {
t.Errorf("TestURL = %q, want an explicit empty value kept so the URL in force survives", empty.TestURL)
}
}
func TestToExternalProviderRejectsUnsupportedProvider(t *testing.T) {
provider, err := toExternalProvider(nil)
if err == nil {
t.Fatal("toExternalProvider accepted a provider it cannot describe")
}
if provider != nil {
t.Errorf("provider = %+v, want nil alongside the error", provider)
}
}
func TestMethodCallDecodeArgumentsRejectsEmptyPayload(t *testing.T) {
tests := map[string]json.RawMessage{
"empty": nil,
"null": json.RawMessage("null"),
}
for name, arguments := range tests {
t.Run(name, func(t *testing.T) {
call := MethodCall{Method: validateConfigMethod, Arguments: arguments}
target := ""
err := call.decodeArguments(&target)
if err == nil {
t.Fatal("decodeArguments accepted a missing payload")
}
if err.Error() != "missing arguments" {
t.Errorf("error = %v, want \"missing arguments\"", err)
}
})
}
}
func TestMethodCallDecodeArgumentsAcceptsScalar(t *testing.T) {
call := MethodCall{
Method: validateConfigMethod,
Arguments: json.RawMessage(`"/tmp/config.yaml"`),
}
target := ""
if err := call.decodeArguments(&target); err != nil {
t.Fatalf("decodeArguments error: %v", err)
}
if target != "/tmp/config.yaml" {
t.Errorf("target = %s, want /tmp/config.yaml", target)
}
}
func TestSideLoadParamsMatchesTheWirePayload(t *testing.T) {
params := SideLoadParams{}
if err := json.Unmarshal([]byte(`{"providerName":"rules","data":"payload"}`), &params); err != nil {
t.Fatalf("decode error: %v", err)
}
if params.ProviderName != "rules" || params.Data != "payload" {
t.Errorf("params = %+v, want {rules payload}", params)
}
}
func TestSideUpdateExternalProviderRejectsUnsupportedProvider(t *testing.T) {
if err := sideUpdateExternalProvider(nil, []byte("payload")); err == nil {
t.Fatal("sideUpdateExternalProvider accepted a provider it cannot side-load")
}
}
func coreMethodConstants(t *testing.T) []CoreMethod {
t.Helper()
fileSet := token.NewFileSet()
file, err := parser.ParseFile(fileSet, "protocol.go", nil, 0)
if err != nil {
t.Fatalf("parse protocol.go: %v", err)
}
var methods []CoreMethod
ast.Inspect(file, func(node ast.Node) bool {
spec, ok := node.(*ast.ValueSpec)
if !ok {
return true
}
typeName, ok := spec.Type.(*ast.Ident)
if !ok || typeName.Name != "CoreMethod" {
return true
}
for _, value := range spec.Values {
literal, ok := value.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
continue
}
unquoted, err := strconv.Unquote(literal.Value)
if err != nil {
t.Fatalf("unquote %s: %v", literal.Value, err)
}
methods = append(methods, CoreMethod(unquoted))
}
return true
})
if len(methods) == 0 {
t.Fatal("found no CoreMethod constants; the parser lost track of protocol.go")
}
return methods
}
func TestEveryCoreMethodConstantIsDispatchable(t *testing.T) {
notDispatched := map[CoreMethod]string{
messageMethod: "core-to-host event envelope, never received",
crashMethod: "handled by the dispatcher so it bypasses panic recovery",
updateDnsMethod: "registered by the Android build only",
}
for _, method := range coreMethodConstants(t) {
if reason, expected := notDispatched[method]; expected {
if _, exists := methodHandlers[method]; exists {
t.Errorf("method %s has a handler but is documented as %s", method, reason)
}
continue
}
if _, exists := methodHandlers[method]; !exists {
t.Errorf("method %s has no handler and would answer not_implemented", method)
}
}
}
func TestRegisterMethodRejectsADuplicate(t *testing.T) {
defer func() {
if recover() == nil {
t.Error("registerMethod accepted a second handler for an already routed method")
}
}()
registerMethod(getProxiesMethod, acknowledged(func() {}))
}
// fakeProxyProvider and fakeRuleProvider stand in for the mihomo providers the
// tunnel holds. Only the name and the vehicle type matter to the lookup; the
// rest of each interface is here because the tunnel maps are typed.
type fakeProxyProvider struct {
name string
vehicle cp.VehicleType
}
func (f *fakeProxyProvider) Name() string { return f.name }
func (f *fakeProxyProvider) VehicleType() cp.VehicleType { return f.vehicle }
func (f *fakeProxyProvider) Type() cp.ProviderType { return cp.Proxy }
func (f *fakeProxyProvider) Initial() error { return nil }
func (f *fakeProxyProvider) Update() error { return nil }
func (f *fakeProxyProvider) Proxies() []constant.Proxy { return nil }
func (f *fakeProxyProvider) Count() int { return 0 }
func (f *fakeProxyProvider) Touch() {}
func (f *fakeProxyProvider) HealthCheck() {}
func (f *fakeProxyProvider) Version() uint32 { return 0 }
func (f *fakeProxyProvider) RegisterHealthCheckTask(string, utils.IntRanges[uint16], string, uint) {
}
func (f *fakeProxyProvider) HealthCheckURL() string { return "" }
type fakeRuleProvider struct {
name string
vehicle cp.VehicleType
}
func (f *fakeRuleProvider) Name() string { return f.name }
func (f *fakeRuleProvider) VehicleType() cp.VehicleType { return f.vehicle }
func (f *fakeRuleProvider) Type() cp.ProviderType { return cp.Rule }
func (f *fakeRuleProvider) Initial() error { return nil }
func (f *fakeRuleProvider) Update() error { return nil }
func (f *fakeRuleProvider) Behavior() cp.RuleBehavior { return cp.Domain }
func (f *fakeRuleProvider) Count() int { return 0 }
func (f *fakeRuleProvider) Match(*constant.Metadata, constant.RuleMatchHelper) bool {
return false
}
func (f *fakeRuleProvider) Strategy() any { return nil }
// The unit tests never apply a config, so the tunnel starts empty and restoring
// it to empty is restoring what was there.
func withTunnelProviders(
t *testing.T,
proxyProviders map[string]cp.ProxyProvider,
ruleProviders map[string]cp.RuleProvider,
) {
t.Helper()
tunnel.UpdateProxies(nil, proxyProviders)
tunnel.UpdateRules(nil, nil, ruleProviders)
t.Cleanup(func() {
tunnel.UpdateProxies(nil, nil)
tunnel.UpdateRules(nil, nil, nil)
})
}
func TestExternalProvidersSkipsInlineProviders(t *testing.T) {
withTunnelProviders(t,
map[string]cp.ProxyProvider{
"subscription": &fakeProxyProvider{name: "subscription", vehicle: cp.HTTP},
"inline": &fakeProxyProvider{name: "inline", vehicle: cp.Compatible},
},
map[string]cp.RuleProvider{
"ruleset": &fakeRuleProvider{name: "ruleset", vehicle: cp.HTTP},
},
)
providers := externalProviders()
if _, exist := providers["inline"]; exist {
t.Error("a compatible provider was reported as externally updatable")
}
if len(providers) != 2 {
t.Errorf("externalProviders() = %d entries, want subscription and ruleset", len(providers))
}
}
func TestLookupExternalProviderFollowsAConfigReplacement(t *testing.T) {
tunnel.UpdateProxies(nil, map[string]cp.ProxyProvider{
"first": &fakeProxyProvider{name: "first", vehicle: cp.HTTP},
})
t.Cleanup(func() { tunnel.UpdateProxies(nil, nil) })
if _, exist := lookupExternalProvider("first"); !exist {
t.Fatal("the installed provider was not found")
}
tunnel.UpdateProxies(nil, map[string]cp.ProxyProvider{
"second": &fakeProxyProvider{name: "second", vehicle: cp.HTTP},
})
if _, exist := lookupExternalProvider("first"); exist {
t.Error("a provider the tunnel no longer holds is still reachable, so updating it writes to disk for nothing")
}
if _, exist := lookupExternalProvider("second"); !exist {
t.Error("the replacement provider is not reachable")
}
}
func TestLookupExternalProviderRejectsInlineAndUnknownNames(t *testing.T) {
withTunnelProviders(t,
map[string]cp.ProxyProvider{
"inline": &fakeProxyProvider{name: "inline", vehicle: cp.Compatible},
},
nil,
)
if _, exist := lookupExternalProvider("inline"); exist {
t.Error("an inline provider is not externally updatable but was handed out")
}
if _, exist := lookupExternalProvider("missing"); exist {
t.Error("an unknown name resolved to a provider")
}
}
// externalProviders writes rule providers last, so one shadows a proxy provider
// sharing its name. The lookup has to agree with the list the host was given.
func TestLookupExternalProviderMatchesTheListedPrecedence(t *testing.T) {
const shared = "same-name"
ruleProvider := &fakeRuleProvider{name: shared, vehicle: cp.HTTP}
withTunnelProviders(t,
map[string]cp.ProxyProvider{shared: &fakeProxyProvider{name: shared, vehicle: cp.HTTP}},
map[string]cp.RuleProvider{shared: ruleProvider},
)
found, exist := lookupExternalProvider(shared)
if !exist {
t.Fatal("the shared name resolved to nothing")
}
if found != externalProviders()[shared] {
t.Error("lookupExternalProvider disagrees with the list externalProviders builds")
}
if found != cp.Provider(ruleProvider) {
t.Error("the proxy provider shadowed the rule provider, reversing the listed precedence")
}
}