fix: sync typed-nil dependency validation
This commit is contained in:
parent
401a8f148e
commit
6e49d83fee
4 changed files with 111 additions and 1 deletions
|
|
@ -854,7 +854,7 @@ func run(logger *zap.Logger) error {
|
||||||
EphemeralPush: ephemeralStore,
|
EphemeralPush: ephemeralStore,
|
||||||
EphemeralReports: ephemeralReportStore,
|
EphemeralReports: ephemeralReportStore,
|
||||||
Users: usersService,
|
Users: usersService,
|
||||||
TelegramLogin: telegramLoginService,
|
TelegramLogin: telegramLoginRPCDependency(telegramLoginService),
|
||||||
Updates: updatesService,
|
Updates: updatesService,
|
||||||
BootstrapUpdates: bootstrapUpdateStore,
|
BootstrapUpdates: bootstrapUpdateStore,
|
||||||
BotAPIUpdates: botAPIUpdateStore,
|
BotAPIUpdates: botAPIUpdateStore,
|
||||||
|
|
@ -1047,6 +1047,17 @@ func run(logger *zap.Logger) error {
|
||||||
return srv.ListenAndServe(ctx, cfg.ListenAddr)
|
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) {
|
func runTelegramLoginRetention(ctx context.Context, service *telegramloginapp.Service, retention, interval time.Duration, batch int, logger *zap.Logger) {
|
||||||
run := func() {
|
run := func() {
|
||||||
var total int64
|
var total int64
|
||||||
|
|
|
||||||
21
cmd/telesrv/main_test.go
Normal file
21
cmd/telesrv/main_test.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
internal/rpc/deps_validation_test.go
Normal file
51
internal/rpc/deps_validation_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -234,6 +235,7 @@ type authUserCacheEntry struct {
|
||||||
|
|
||||||
// New 创建 Router,由各业务域自行注册其 RPC handler(registerHelp/Auth/Users/Updates)。
|
// New 创建 Router,由各业务域自行注册其 RPC handler(registerHelp/Auth/Users/Updates)。
|
||||||
func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
|
func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
|
||||||
|
assertNoTypedNilDeps(deps)
|
||||||
instanceID := cfg.InstanceID
|
instanceID := cfg.InstanceID
|
||||||
if instanceID == "" {
|
if instanceID == "" {
|
||||||
instanceID = fmt.Sprintf("%016x", randomNonZeroInt64())
|
instanceID = fmt.Sprintf("%016x", randomNonZeroInt64())
|
||||||
|
|
@ -282,6 +284,31 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router {
|
||||||
return r
|
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)) {
|
func registerRPC[T bin.Object](d *tlprofile.Dispatcher, method tlprofile.SemanticID, handler func(context.Context, T) (any, error)) {
|
||||||
if d == nil || handler == nil {
|
if d == nil || handler == nil {
|
||||||
panic("rpc: register nil canonical RPC handler or dispatcher")
|
panic("rpc: register nil canonical RPC handler or dispatcher")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue