Files
larksuite-cli/internal/event/registry_test.go
sang-neo03 ed7fdd1a27 feat: optimize event subscription precheck, links, and consumer guard (#1447)
* feat: add SubscriptionType and SingleConsumer to EventKey definition

* feat: fetch subscribed callbacks from application/get

* feat: build addons scan-to-enable deep link for event precheck

* feat: route callback precheck to application/get and emit scan links

* feat: add reject fields to hello_ack protocol message

* feat: add exclusive registration to event bus hub

* feat: reject duplicate consumer for SingleConsumer EventKey at bus handshake

* feat: surface bus consumer rejection as failed_precondition error

* fix: encode empty addons sides as [] not null per launcher contract

* fix: report missing callbacks when console has none subscribed

* feat: bound exclusive consumer cleanup wait with configurable timeout

* refactor: drain exclusive-wait timer and document websocket-only callbacks

* fix: use camelCase clientID param in event scan-to-enable link

* test: cover null/omitted callbacks and assert typed error category

* fix: keep auth login remediation for user-identity missing scopes

* refactor: simplify SubscriptionType normalization to match validateAuth style
2026-06-16 19:41:52 +08:00

302 lines
8.1 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package event
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
)
func mustPanic(t *testing.T, substring string) {
t.Helper()
r := recover()
if r == nil {
t.Fatal("expected panic, got none")
}
msg, _ := r.(string)
if msg == "" {
if err, ok := r.(error); ok {
msg = err.Error()
} else {
msg = fmt.Sprintf("%v", r)
}
}
if !strings.Contains(msg, substring) {
t.Errorf("panic %q does not contain %q", msg, substring)
}
}
type emptyOut struct {
A string `json:"a"`
}
func nativeSchema() SchemaDef {
return SchemaDef{Native: &SchemaSpec{Type: reflect.TypeOf(emptyOut{})}}
}
func customSchema() SchemaDef {
return SchemaDef{Custom: &SchemaSpec{Type: reflect.TypeOf(emptyOut{})}}
}
func customProcess() func(context.Context, APIClient, *RawEvent, map[string]string) (json.RawMessage, error) {
return func(context.Context, APIClient, *RawEvent, map[string]string) (json.RawMessage, error) {
return nil, nil
}
}
func TestRegisterKey_NativeOnly(t *testing.T) {
resetRegistry()
RegisterKey(KeyDefinition{
Key: "t.native",
EventType: "t.native",
Schema: nativeSchema(),
})
def, ok := Lookup("t.native")
if !ok {
t.Fatal("Lookup failed")
}
if def.Schema.Native == nil {
t.Fatal("Native not stored")
}
if def.Process != nil {
t.Error("Process should be nil for Native")
}
}
func TestRegisterKey_CustomWithProcess(t *testing.T) {
resetRegistry()
RegisterKey(KeyDefinition{
Key: "t.custom",
EventType: "t.custom",
Schema: customSchema(),
Process: customProcess(),
})
def, ok := Lookup("t.custom")
if !ok {
t.Fatal("Lookup failed")
}
if def.Schema.Custom == nil {
t.Fatal("Custom not stored")
}
if def.Process == nil {
t.Error("Process should be set")
}
}
func TestRegisterKey_DuplicatePanics(t *testing.T) {
resetRegistry()
RegisterKey(KeyDefinition{Key: "t.dup", EventType: "t.dup", Schema: nativeSchema()})
defer mustPanic(t, "duplicate EventKey")
RegisterKey(KeyDefinition{Key: "t.dup", EventType: "t.dup", Schema: nativeSchema()})
}
func TestRegisterKey_EmptyEventTypePanics(t *testing.T) {
resetRegistry()
defer mustPanic(t, "EventType must not be empty")
RegisterKey(KeyDefinition{Key: "t.no_type", Schema: nativeSchema()})
}
func TestRegisterKey_PanicsWhenBothSchemasSet(t *testing.T) {
resetRegistry()
defer mustPanic(t, "mutually exclusive")
RegisterKey(KeyDefinition{
Key: "t.both",
EventType: "t.both",
Schema: SchemaDef{
Native: &SchemaSpec{Type: reflect.TypeOf(emptyOut{})},
Custom: &SchemaSpec{Type: reflect.TypeOf(emptyOut{})},
},
})
}
func TestRegisterKey_PanicsWhenNoSchemaSet(t *testing.T) {
resetRegistry()
defer mustPanic(t, "Schema requires either Native or Custom")
RegisterKey(KeyDefinition{Key: "t.empty", EventType: "t.empty"})
}
func TestRegisterKey_PanicsWhenNativeWithProcess(t *testing.T) {
resetRegistry()
defer mustPanic(t, "Schema.Native forbids Process")
RegisterKey(KeyDefinition{
Key: "t.badcombo",
EventType: "t.badcombo",
Schema: nativeSchema(),
Process: customProcess(),
})
}
func TestRegisterKey_PanicsWhenSpecHasBothTypeAndRaw(t *testing.T) {
resetRegistry()
defer mustPanic(t, "requires exactly one of Type or Raw")
RegisterKey(KeyDefinition{
Key: "t.bothsrc",
EventType: "t.bothsrc",
Schema: SchemaDef{
Custom: &SchemaSpec{Type: reflect.TypeOf(emptyOut{}), Raw: json.RawMessage(`{}`)},
},
})
}
func TestRegisterKey_PanicsWhenSpecHasNeitherTypeNorRaw(t *testing.T) {
resetRegistry()
defer mustPanic(t, "requires exactly one of Type or Raw")
RegisterKey(KeyDefinition{
Key: "t.nosrc",
EventType: "t.nosrc",
Schema: SchemaDef{
Custom: &SchemaSpec{},
},
})
}
func TestRegisterKey_ParamMultiRequiresValues(t *testing.T) {
resetRegistry()
defer mustPanic(t, "requires Values")
RegisterKey(KeyDefinition{
Key: "t.paramnovalues",
EventType: "t.paramnovalues",
Schema: nativeSchema(),
Params: []ParamDef{{Name: "fields", Type: ParamMulti}},
})
}
func TestRegisterKey_ParamEnumRequiresValues(t *testing.T) {
resetRegistry()
defer mustPanic(t, "requires Values")
RegisterKey(KeyDefinition{
Key: "t.enumnovalues",
EventType: "t.enumnovalues",
Schema: nativeSchema(),
Params: []ParamDef{{Name: "mode", Type: ParamEnum}},
})
}
func TestRegisterKey_ParamValueRequiresDesc(t *testing.T) {
resetRegistry()
defer mustPanic(t, "requires non-empty Desc")
RegisterKey(KeyDefinition{
Key: "t.paramdesc",
EventType: "t.paramdesc",
Schema: nativeSchema(),
Params: []ParamDef{{
Name: "f",
Type: ParamEnum,
Values: []ParamValue{{Value: "x"}},
}},
})
}
func TestRegisterKey_UnknownParamType(t *testing.T) {
resetRegistry()
defer mustPanic(t, "unknown type")
RegisterKey(KeyDefinition{
Key: "t.badtype",
EventType: "t.badtype",
Schema: nativeSchema(),
Params: []ParamDef{{Name: "x", Type: ParamType("wtf")}},
})
}
func TestRegisterKey_InvalidAuthTypesPanics(t *testing.T) {
resetRegistry()
defer mustPanic(t, "AuthTypes elements must be")
RegisterKey(KeyDefinition{
Key: "t.badauth",
EventType: "t.badauth",
Schema: nativeSchema(),
AuthTypes: []string{"invalid"},
})
}
func TestRegisterKey_ValidAuthTypes(t *testing.T) {
resetRegistry()
RegisterKey(KeyDefinition{Key: "u.e", EventType: "u.e", Schema: nativeSchema(), AuthTypes: []string{"user"}})
RegisterKey(KeyDefinition{Key: "b.e", EventType: "b.e", Schema: nativeSchema(), AuthTypes: []string{"bot"}})
RegisterKey(KeyDefinition{Key: "ub.e", EventType: "ub.e", Schema: nativeSchema(), AuthTypes: []string{"bot", "user"}})
RegisterKey(KeyDefinition{Key: "na.e", EventType: "na.e", Schema: nativeSchema()})
}
func TestListAll_SortedByKey(t *testing.T) {
resetRegistry()
RegisterKey(KeyDefinition{Key: "z.event", EventType: "z", Schema: nativeSchema()})
RegisterKey(KeyDefinition{Key: "a.event", EventType: "a", Schema: nativeSchema()})
RegisterKey(KeyDefinition{Key: "m.event", EventType: "m", Schema: nativeSchema()})
all := ListAll()
if len(all) != 3 || all[0].Key != "a.event" || all[1].Key != "m.event" || all[2].Key != "z.event" {
t.Errorf("keys not sorted: %v", []string{all[0].Key, all[1].Key, all[2].Key})
}
}
func TestBufferSize_Clamped(t *testing.T) {
resetRegistry()
RegisterKey(KeyDefinition{
Key: "big", EventType: "big", Schema: nativeSchema(),
BufferSize: 5000,
})
def, _ := Lookup("big")
if def.BufferSize != MaxBufferSize {
t.Errorf("BufferSize = %d, want %d", def.BufferSize, MaxBufferSize)
}
}
func TestRegisterKey_SubscriptionTypeDefaultsToEvent(t *testing.T) {
const key = "test.subtype.default"
RegisterKey(KeyDefinition{
Key: key,
EventType: key,
Schema: SchemaDef{Native: &SchemaSpec{Raw: []byte(`{"type":"object"}`)}},
})
defer UnregisterKeyForTest(key)
def, ok := Lookup(key)
if !ok {
t.Fatalf("Lookup(%q) failed", key)
}
if def.SubscriptionType != SubTypeEvent {
t.Errorf("SubscriptionType = %q, want %q", def.SubscriptionType, SubTypeEvent)
}
if def.SingleConsumer {
t.Errorf("SingleConsumer = true, want false (default)")
}
}
func TestRegisterKey_SubscriptionTypeCallbackPreserved(t *testing.T) {
const key = "test.subtype.callback"
RegisterKey(KeyDefinition{
Key: key,
EventType: key,
SubscriptionType: SubTypeCallback,
SingleConsumer: true,
Schema: SchemaDef{Native: &SchemaSpec{Raw: []byte(`{"type":"object"}`)}},
})
defer UnregisterKeyForTest(key)
def, _ := Lookup(key)
if def.SubscriptionType != SubTypeCallback {
t.Errorf("SubscriptionType = %q, want %q", def.SubscriptionType, SubTypeCallback)
}
if !def.SingleConsumer {
t.Errorf("SingleConsumer = false, want true")
}
}
func TestRegisterKey_InvalidSubscriptionTypePanics(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("expected panic for invalid SubscriptionType")
}
}()
RegisterKey(KeyDefinition{
Key: "test.subtype.bogus",
EventType: "test.subtype.bogus",
SubscriptionType: "bogus",
Schema: SchemaDef{Native: &SchemaSpec{Raw: []byte(`{"type":"object"}`)}},
})
}