From 6e49d83fee0934688339666749665421359c3119 Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 16:09:27 +0800 Subject: [PATCH] fix: sync typed-nil dependency validation --- cmd/telesrv/main.go | 13 ++++++- cmd/telesrv/main_test.go | 21 ++++++++++++ internal/rpc/deps_validation_test.go | 51 ++++++++++++++++++++++++++++ internal/rpc/router.go | 27 +++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 cmd/telesrv/main_test.go create mode 100644 internal/rpc/deps_validation_test.go diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index a78c8ca0..ac50180e 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -854,7 +854,7 @@ func run(logger *zap.Logger) error { EphemeralPush: ephemeralStore, EphemeralReports: ephemeralReportStore, Users: usersService, - TelegramLogin: telegramLoginService, + TelegramLogin: telegramLoginRPCDependency(telegramLoginService), Updates: updatesService, BootstrapUpdates: bootstrapUpdateStore, BotAPIUpdates: botAPIUpdateStore, @@ -1047,6 +1047,17 @@ func run(logger *zap.Logger) error { return srv.ListenAndServe(ctx, cfg.ListenAddr) } +// telegramLoginRPCDependency preserves a disabled Telegram Login service as a +// nil interface. Assigning the nil *Service directly to rpc.Deps would create a +// non-nil interface with a nil concrete pointer and bypass Router availability +// checks. +func telegramLoginRPCDependency(service *telegramloginapp.Service) rpc.TelegramLoginService { + if service == nil { + return nil + } + return service +} + func runTelegramLoginRetention(ctx context.Context, service *telegramloginapp.Service, retention, interval time.Duration, batch int, logger *zap.Logger) { run := func() { var total int64 diff --git a/cmd/telesrv/main_test.go b/cmd/telesrv/main_test.go new file mode 100644 index 00000000..4850bcf9 --- /dev/null +++ b/cmd/telesrv/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" + + telegramloginapp "telesrv/internal/app/telegramlogin" +) + +func TestTelegramLoginRPCDependencyPreservesDisabledNil(t *testing.T) { + var disabled *telegramloginapp.Service + if dependency := telegramLoginRPCDependency(disabled); dependency != nil { + t.Fatalf("disabled Telegram Login dependency = %#v, want nil interface", dependency) + } +} + +func TestTelegramLoginRPCDependencyPreservesEnabledService(t *testing.T) { + enabled := new(telegramloginapp.Service) + if dependency := telegramLoginRPCDependency(enabled); dependency != enabled { + t.Fatalf("enabled Telegram Login dependency = %#v, want %p", dependency, enabled) + } +} diff --git a/internal/rpc/deps_validation_test.go b/internal/rpc/deps_validation_test.go new file mode 100644 index 00000000..42b51678 --- /dev/null +++ b/internal/rpc/deps_validation_test.go @@ -0,0 +1,51 @@ +package rpc + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/iamxvbaba/td/clock" + "go.uber.org/zap" + + telegramloginapp "telesrv/internal/app/telegramlogin" +) + +func TestAssertNoTypedNilDepsRejectsTelegramLogin(t *testing.T) { + var service *telegramloginapp.Service + defer func() { + value := recover() + if value == nil { + t.Fatal("assertNoTypedNilDeps accepted a typed-nil Telegram Login service") + } + message := fmt.Sprint(value) + if !strings.Contains(message, "dependency TelegramLogin is a typed nil *telegramlogin.Service") { + t.Fatalf("panic = %q, want TelegramLogin typed-nil diagnostic", message) + } + }() + New(Config{}, Deps{TelegramLogin: service}, zap.NewNop(), clock.System) +} + +func TestAssertNoTypedNilDepsAcceptsAbsentTelegramLogin(t *testing.T) { + assertNoTypedNilDeps(Deps{}) +} + +func TestDisabledTelegramLoginWebAuthorizationRPCs(t *testing.T) { + router := New(Config{}, Deps{}, zap.NewNop(), clock.System) + ctx := WithUserID(context.Background(), 42) + + listed, err := router.onAccountGetWebAuthorizations(ctx) + if err != nil { + t.Fatalf("get disabled web authorizations: %v", err) + } + if len(listed.Authorizations) != 0 || len(listed.Users) != 0 { + t.Fatalf("disabled web authorizations = %#v, want empty vectors", listed) + } + if reset, err := router.onAccountResetWebAuthorization(ctx, 123); err != nil || !reset { + t.Fatalf("reset disabled web authorization = %v, %v; want true, nil", reset, err) + } + if reset, err := router.onAccountResetWebAuthorizations(ctx); err != nil || !reset { + t.Fatalf("reset all disabled web authorizations = %v, %v; want true, nil", reset, err) + } +} diff --git a/internal/rpc/router.go b/internal/rpc/router.go index 1bf9b11e..212fc3b1 100644 --- a/internal/rpc/router.go +++ b/internal/rpc/router.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "reflect" "sync" "time" @@ -234,6 +235,7 @@ type authUserCacheEntry struct { // New 创建 Router,由各业务域自行注册其 RPC handler(registerHelp/Auth/Users/Updates)。 func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router { + assertNoTypedNilDeps(deps) instanceID := cfg.InstanceID if instanceID == "" { instanceID = fmt.Sprintf("%016x", randomNonZeroInt64()) @@ -282,6 +284,31 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router { return r } +// assertNoTypedNilDeps rejects partially constructed optional dependencies at +// the composition boundary. A Go interface containing a nil concrete pointer +// is not equal to nil, so handler-level availability checks would otherwise +// admit it and panic only when the first method is invoked. +// +// This is an invariant check, not a compatibility fallback: callers must +// either inject a fully constructed implementation or leave the interface nil. +func assertNoTypedNilDeps(deps Deps) { + value := reflect.ValueOf(deps) + typeOfDeps := value.Type() + for i := 0; i < value.NumField(); i++ { + field := value.Field(i) + if field.Kind() != reflect.Interface || field.IsNil() { + continue + } + implementation := field.Elem() + switch implementation.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + if implementation.IsNil() { + panic(fmt.Sprintf("rpc: dependency %s is a typed nil %s", typeOfDeps.Field(i).Name, implementation.Type())) + } + } + } +} + func registerRPC[T bin.Object](d *tlprofile.Dispatcher, method tlprofile.SemanticID, handler func(context.Context, T) (any, error)) { if d == nil || handler == nil { panic("rpc: register nil canonical RPC handler or dispatcher")