mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
* 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
113 lines
3.7 KiB
Go
113 lines
3.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package event
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/core"
|
|
eventlib "github.com/larksuite/cli/internal/event"
|
|
)
|
|
|
|
func decodeAddons(t *testing.T, encoded string) ManifestAddons {
|
|
t.Helper()
|
|
gz, err := base64.RawURLEncoding.DecodeString(encoded)
|
|
if err != nil {
|
|
t.Fatalf("base64url decode: %v", err)
|
|
}
|
|
zr, err := gzip.NewReader(bytes.NewReader(gz))
|
|
if err != nil {
|
|
t.Fatalf("gzip reader: %v", err)
|
|
}
|
|
raw, err := io.ReadAll(zr)
|
|
if err != nil {
|
|
t.Fatalf("gunzip: %v", err)
|
|
}
|
|
var a ManifestAddons
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
t.Fatalf("json: %v", err)
|
|
}
|
|
return a
|
|
}
|
|
|
|
func TestEncodeAddons_RoundTrip(t *testing.T) {
|
|
in := ManifestAddons{Scopes: &AddonsScopes{Tenant: []string{"im:message"}}}
|
|
encoded, err := encodeAddons(in)
|
|
if err != nil {
|
|
t.Fatalf("encode: %v", err)
|
|
}
|
|
for _, r := range encoded {
|
|
if !(r == '-' || r == '_' || (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')) {
|
|
t.Fatalf("encoded contains non-base64url char %q in %q", r, encoded)
|
|
}
|
|
}
|
|
out := decodeAddons(t, encoded)
|
|
if out.Scopes == nil || len(out.Scopes.Tenant) != 1 || out.Scopes.Tenant[0] != "im:message" {
|
|
t.Errorf("roundtrip mismatch: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestConsoleAddonsURL_FormatAndBrandHost(t *testing.T) {
|
|
url, err := consoleAddonsURL(core.BrandFeishu, "cli_x", ManifestAddons{Callbacks: &AddonsCallbacks{Items: []string{"card.action.trigger"}}})
|
|
if err != nil {
|
|
t.Fatalf("url: %v", err)
|
|
}
|
|
host := core.ResolveEndpoints(core.BrandFeishu).Open
|
|
prefix := host + "/page/launcher?clientID=cli_x&addons="
|
|
if !strings.HasPrefix(url, prefix) {
|
|
t.Errorf("url = %q, want prefix %q", url, prefix)
|
|
}
|
|
out := decodeAddons(t, strings.TrimPrefix(url, prefix))
|
|
if out.Callbacks == nil || len(out.Callbacks.Items) != 1 || out.Callbacks.Items[0] != "card.action.trigger" {
|
|
t.Errorf("decoded callbacks mismatch: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestMissingScopeAddons_ByIdentity(t *testing.T) {
|
|
bot := missingScopeAddons(core.AsBot, []string{"im:message"})
|
|
if bot.Scopes == nil || len(bot.Scopes.Tenant) != 1 || len(bot.Scopes.User) != 0 {
|
|
t.Errorf("bot scopes = %+v, want tenant-only", bot.Scopes)
|
|
}
|
|
user := missingScopeAddons(core.AsUser, []string{"im:message"})
|
|
if user.Scopes == nil || len(user.Scopes.User) != 1 || len(user.Scopes.Tenant) != 0 {
|
|
t.Errorf("user scopes = %+v, want user-only", user.Scopes)
|
|
}
|
|
}
|
|
|
|
func TestMissingSubscriptionAddons_EventVsCallback(t *testing.T) {
|
|
ev := missingSubscriptionAddons(eventlib.SubTypeEvent, core.AsBot, []string{"im.message.receive_v1"})
|
|
if ev.Events == nil || len(ev.Events.Items.Tenant) != 1 {
|
|
t.Errorf("event addons = %+v, want events.items.tenant", ev.Events)
|
|
}
|
|
cb := missingSubscriptionAddons(eventlib.SubTypeCallback, core.AsBot, []string{"card.action.trigger"})
|
|
if cb.Callbacks == nil || len(cb.Callbacks.Items) != 1 || cb.Events != nil {
|
|
t.Errorf("callback addons = %+v, want callbacks.items only", cb)
|
|
}
|
|
}
|
|
|
|
func TestMissingAddons_EncodeEmptyArraysNotNull(t *testing.T) {
|
|
// Unused identity sides must encode as [] (not null) so the launcher page's
|
|
// shape validation treats them as "缺省 -> 空数组" per the addons spec.
|
|
cases := []ManifestAddons{
|
|
missingScopeAddons(core.AsBot, []string{"im:message"}),
|
|
missingScopeAddons(core.AsUser, []string{"im:message"}),
|
|
missingSubscriptionAddons(eventlib.SubTypeEvent, core.AsBot, []string{"im.message.receive_v1"}),
|
|
}
|
|
for i, a := range cases {
|
|
raw, err := json.Marshal(a)
|
|
if err != nil {
|
|
t.Fatalf("case %d marshal: %v", i, err)
|
|
}
|
|
if bytes.Contains(raw, []byte("null")) {
|
|
t.Errorf("case %d encodes a null array, want []: %s", i, raw)
|
|
}
|
|
}
|
|
}
|