fix: sync typed-nil dependency validation

This commit is contained in:
A 2026-07-22 16:09:27 +08:00
parent 401a8f148e
commit 6e49d83fee
4 changed files with 111 additions and 1 deletions

View file

@ -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)
}
}

View file

@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"reflect"
"sync"
"time"
@ -234,6 +235,7 @@ type authUserCacheEntry struct {
// New 创建 Router由各业务域自行注册其 RPC handlerregisterHelp/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")