Files
larksuite-cli/internal/event/protocol/messages_test.go
liuxinyanglxy 3f77eded9d feat: per-resource subscription identity + Match hook (#1185)
Framework support for resource-scoped event subscriptions, so one
EventKey can fan out into independent per-resource subscription scopes:

- KeyDefinition gains SubscriptionKey / NormalizeParams / Match hooks
- ComputeSubscriptionID derives a dedup identity from (EventKey, sub-key
  params); plumbed through bus Hub, consume loop, and the
  Hello / PreShutdownCheck / ConsumerInfo protocol messages
- add a synchronous Match filter stage before Process
- change PreConsume cleanup to func() error and surface cleanup
  (unsubscribe) failures as WARN with an idempotency note
- adapt minutes/vc/whiteboard PreConsume to the new cleanup signature
- render SubscriptionID / SubscriptionKey in event status & schema output

No domain wires these hooks yet; covered by unit tests using bus/protocol
doubles. (Mail, the original exerciser, is intentionally not included.)

Change-Id: Ifc743f1aa0bc4dff0c8a1e35da24883694fe7699
2026-06-11 16:22:04 +08:00

108 lines
3.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package protocol
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"testing"
"time"
)
// Every NewXxx helper must set the Type discriminator (Decode rejects messages without it).
func TestConstructors_PinTypeField(t *testing.T) {
if got := NewHello(1, "k", []string{"t"}, "v1", ""); got.Type != MsgTypeHello {
t.Errorf("NewHello.Type = %q, want %q", got.Type, MsgTypeHello)
}
if got := NewHelloAck("v1", true); got.Type != MsgTypeHelloAck || !got.FirstForKey {
t.Errorf("NewHelloAck mismatch: %+v", got)
}
if got := NewEvent("im.msg", "e1", "", 7, json.RawMessage(`{}`)); got.Type != MsgTypeEvent || got.Seq != 7 {
t.Errorf("NewEvent mismatch: %+v", got)
}
if got := NewPreShutdownCheck("k", ""); got.Type != MsgTypePreShutdownCheck || got.EventKey != "k" {
t.Errorf("NewPreShutdownCheck mismatch: %+v", got)
}
if got := NewPreShutdownAck(true); got.Type != MsgTypePreShutdownAck || !got.LastForKey {
t.Errorf("NewPreShutdownAck mismatch: %+v", got)
}
if got := NewStatusQuery(); got.Type != MsgTypeStatusQuery {
t.Errorf("NewStatusQuery.Type = %q", got.Type)
}
if got := NewStatusResponse(42, 10, 2, []ConsumerInfo{{PID: 1}, {PID: 2}}); got.Type != MsgTypeStatusResponse || got.PID != 42 || len(got.Consumers) != 2 {
t.Errorf("NewStatusResponse mismatch: %+v", got)
}
if got := NewShutdown(); got.Type != MsgTypeShutdown {
t.Errorf("NewShutdown.Type = %q", got.Type)
}
if got := NewSourceStatus("feishu-ws", SourceStateConnected, "ok"); got.Type != MsgTypeSourceStatus || got.Detail != "ok" {
t.Errorf("NewSourceStatus mismatch: %+v", got)
}
}
func TestEncode_DecodeRoundtripAllTypes(t *testing.T) {
roundtrip := func(t *testing.T, msg interface{}, want interface{}) {
t.Helper()
var buf bytes.Buffer
if err := Encode(&buf, msg); err != nil {
t.Fatalf("encode: %v", err)
}
line := bytes.TrimRight(buf.Bytes(), "\n")
got, err := Decode(line)
if err != nil {
t.Fatalf("decode: %v", err)
}
if gotT, wantT := fmt.Sprintf("%T", got), fmt.Sprintf("%T", want); gotT != wantT {
t.Errorf("decoded type = %s, want %s", gotT, wantT)
}
}
roundtrip(t, NewHelloAck("v1", true), &HelloAck{})
roundtrip(t, NewPreShutdownCheck("im.msg", ""), &PreShutdownCheck{})
roundtrip(t, NewPreShutdownAck(false), &PreShutdownAck{})
roundtrip(t, NewStatusQuery(), &StatusQuery{})
roundtrip(t, NewStatusResponse(7, 120, 1, []ConsumerInfo{{PID: 99, EventKey: "k"}}), &StatusResponse{})
roundtrip(t, NewShutdown(), &Shutdown{})
roundtrip(t, NewSourceStatus("feishu", SourceStateReconnecting, "attempt 2"), &SourceStatus{})
roundtrip(t, &Bye{Type: MsgTypeBye}, &Bye{})
}
// EncodeWithDeadline must apply a write deadline so a wedged peer can't stall the writer forever.
func TestEncodeWithDeadline_AppliesDeadline(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
start := time.Now()
err := EncodeWithDeadline(client, NewShutdown(), 100*time.Millisecond)
elapsed := time.Since(start)
if err == nil {
t.Fatal("expected deadline error, got nil")
}
if elapsed > 500*time.Millisecond {
t.Errorf("EncodeWithDeadline didn't honour deadline: took %v (want ~100ms)", elapsed)
}
}
func TestReadFrame_RejectsOversized(t *testing.T) {
big := bytes.Repeat([]byte("a"), MaxFrameBytes+1)
big = append(big, '\n')
br := bufio.NewReader(bytes.NewReader(big))
_, err := ReadFrame(br)
if !errors.Is(err, ErrFrameTooLarge) {
t.Fatalf("ReadFrame on oversized input: err = %v, want ErrFrameTooLarge", err)
}
}
func TestReadFrame_PropagatesEOF(t *testing.T) {
br := bufio.NewReader(bytes.NewReader(nil))
_, err := ReadFrame(br)
if err != io.EOF {
t.Errorf("err = %v, want io.EOF", err)
}
}