bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
213 lines
7.5 KiB
Go
213 lines
7.5 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package whiteboard
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/event"
|
|
)
|
|
|
|
// recordedCall captures a single APIClient invocation for assertion.
|
|
type recordedCall struct {
|
|
method string
|
|
path string
|
|
body interface{}
|
|
}
|
|
|
|
// fakeAPIClient is a minimal event.APIClient stub that records calls and
|
|
// can be configured to fail when the request path matches errOnPath.
|
|
type fakeAPIClient struct {
|
|
mu sync.Mutex
|
|
calls []recordedCall
|
|
errOnPath string
|
|
}
|
|
|
|
// CallAPI records the invocation and optionally returns a simulated error
|
|
// when the path contains the configured errOnPath substring.
|
|
func (f *fakeAPIClient) CallAPI(_ context.Context, method, path string, body interface{}) (json.RawMessage, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, recordedCall{method: method, path: path, body: body})
|
|
if f.errOnPath != "" && strings.Contains(path, f.errOnPath) {
|
|
return nil, errors.New("simulated subscribe failure")
|
|
}
|
|
return json.RawMessage(`{}`), nil
|
|
}
|
|
|
|
// TestWhiteboardSubscriptionPreConsume_MissingWhiteboardID verifies that the
|
|
// PreConsume hook fails fast with an actionable error when whiteboard_id
|
|
// is absent from the params map.
|
|
func TestWhiteboardSubscriptionPreConsume_MissingWhiteboardID(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
pc := whiteboardSubscriptionPreConsume(eventTypeWhiteboardUpdated)
|
|
cleanup, err := pc(context.Background(), &fakeAPIClient{}, map[string]string{})
|
|
if err == nil {
|
|
t.Fatalf("expected error when whiteboard_id missing")
|
|
}
|
|
if cleanup != nil {
|
|
t.Fatalf("expected nil cleanup on error")
|
|
}
|
|
if !strings.Contains(err.Error(), "whiteboard_id") {
|
|
t.Fatalf("error should mention whiteboard_id, got: %v", err)
|
|
}
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T: %v", err, err)
|
|
}
|
|
if ve.Subtype != errs.SubtypeInvalidArgument || ve.Param != "--param" {
|
|
t.Errorf("subtype/param = %s/%q, want %s/%q", ve.Subtype, ve.Param, errs.SubtypeInvalidArgument, "--param")
|
|
}
|
|
if ve.Hint == "" {
|
|
t.Error("missing whiteboard_id should carry a hint")
|
|
}
|
|
}
|
|
|
|
// TestWhiteboardSubscriptionPreConsume_NilRuntime verifies that PreConsume
|
|
// returns an error when the runtime APIClient dependency is missing.
|
|
func TestWhiteboardSubscriptionPreConsume_NilRuntime(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
pc := whiteboardSubscriptionPreConsume(eventTypeWhiteboardUpdated)
|
|
_, err := pc(context.Background(), nil, map[string]string{"whiteboard_id": "wb1"})
|
|
if err == nil {
|
|
t.Fatalf("expected error when runtime client is nil")
|
|
}
|
|
if p, ok := errs.ProblemOf(err); !ok || p.Category != errs.CategoryInternal {
|
|
t.Errorf("nil-runtime invariant should be a typed internal error, got %T: %v", err, err)
|
|
}
|
|
}
|
|
|
|
// TestWhiteboardSubscriptionPreConsume_SubscribeError verifies that a
|
|
// failed subscribe call surfaces the error and skips registering a cleanup,
|
|
// so no spurious unsubscribe is invoked.
|
|
func TestWhiteboardSubscriptionPreConsume_SubscribeError(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
pc := whiteboardSubscriptionPreConsume(eventTypeWhiteboardUpdated)
|
|
rt := &fakeAPIClient{errOnPath: "/subscribe"}
|
|
cleanup, err := pc(context.Background(), rt, map[string]string{"whiteboard_id": "wb1"})
|
|
if err == nil {
|
|
t.Fatalf("expected error from subscribe call")
|
|
}
|
|
if cleanup != nil {
|
|
t.Fatalf("expected nil cleanup when subscribe fails")
|
|
}
|
|
// only the failed subscribe call should have been made; no unsubscribe.
|
|
if len(rt.calls) != 1 {
|
|
t.Fatalf("expected exactly 1 call (subscribe), got %d", len(rt.calls))
|
|
}
|
|
}
|
|
|
|
// TestWhiteboardSubscriptionPreConsume_SubscribeAndCleanup verifies the full
|
|
// happy-path: subscribe is called once with the correct method/path/body,
|
|
// and the returned cleanup invokes the matching unsubscribe.
|
|
func TestWhiteboardSubscriptionPreConsume_SubscribeAndCleanup(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
pc := whiteboardSubscriptionPreConsume(eventTypeWhiteboardUpdated)
|
|
rt := &fakeAPIClient{}
|
|
cleanup, err := pc(context.Background(), rt, map[string]string{"whiteboard_id": "wb1"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cleanup == nil {
|
|
t.Fatalf("expected non-nil cleanup")
|
|
}
|
|
|
|
if len(rt.calls) != 1 {
|
|
t.Fatalf("expected 1 call after subscribe, got %d", len(rt.calls))
|
|
}
|
|
got := rt.calls[0]
|
|
if got.method != "POST" {
|
|
t.Errorf("subscribe method: got %q, want POST", got.method)
|
|
}
|
|
wantSubPath := "/open-apis/board/v1/whiteboards/wb1/subscribe"
|
|
if got.path != wantSubPath {
|
|
t.Errorf("subscribe path: got %q, want %q", got.path, wantSubPath)
|
|
}
|
|
body, _ := got.body.(map[string]string)
|
|
if body["event_type"] != eventTypeWhiteboardUpdated {
|
|
t.Errorf("subscribe body event_type: got %q, want %q", body["event_type"], eventTypeWhiteboardUpdated)
|
|
}
|
|
|
|
cleanup()
|
|
if len(rt.calls) != 2 {
|
|
t.Fatalf("expected 2 calls after cleanup, got %d", len(rt.calls))
|
|
}
|
|
got2 := rt.calls[1]
|
|
if got2.method != "POST" {
|
|
t.Errorf("unsubscribe method: got %q, want POST", got2.method)
|
|
}
|
|
wantUnsubPath := "/open-apis/board/v1/whiteboards/wb1/unsubscribe"
|
|
if got2.path != wantUnsubPath {
|
|
t.Errorf("unsubscribe path: got %q, want %q", got2.path, wantUnsubPath)
|
|
}
|
|
body2, _ := got2.body.(map[string]string)
|
|
if body2["event_type"] != eventTypeWhiteboardUpdated {
|
|
t.Errorf("unsubscribe body event_type: got %q, want %q", body2["event_type"], eventTypeWhiteboardUpdated)
|
|
}
|
|
}
|
|
|
|
// TestWhiteboardSubscriptionPreConsume_PathSegmentEncoded verifies that
|
|
// whiteboard_id values containing reserved URL characters are properly
|
|
// path-segment encoded so they cannot escape into adjacent path segments.
|
|
func TestWhiteboardSubscriptionPreConsume_PathSegmentEncoded(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
pc := whiteboardSubscriptionPreConsume(eventTypeWhiteboardUpdated)
|
|
rt := &fakeAPIClient{}
|
|
// 含特殊字符的 whiteboard_id 应被 path-segment 编码,避免越界到其他 path 段。
|
|
_, err := pc(context.Background(), rt, map[string]string{"whiteboard_id": "wb/1?evil"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(rt.calls) != 1 {
|
|
t.Fatalf("expected 1 call, got %d", len(rt.calls))
|
|
}
|
|
if strings.Contains(rt.calls[0].path, "wb/1?evil") {
|
|
t.Errorf("whiteboard_id was not encoded; path: %s", rt.calls[0].path)
|
|
}
|
|
}
|
|
|
|
// TestWhiteboardUpdatedV1HasPreConsume ensures the registered EventKey for
|
|
// board.whiteboard.updated_v1 wires the PreConsume hook and declares the
|
|
// required whiteboard_id parameter.
|
|
func TestWhiteboardUpdatedV1HasPreConsume(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
keys := Keys()
|
|
for _, k := range keys {
|
|
if k.Key == eventTypeWhiteboardUpdated {
|
|
if k.PreConsume == nil {
|
|
t.Fatalf("EventKey %s should have PreConsume hook", eventTypeWhiteboardUpdated)
|
|
}
|
|
if len(k.Params) == 0 {
|
|
t.Fatalf("EventKey %s should declare whiteboard_id param", eventTypeWhiteboardUpdated)
|
|
}
|
|
var found bool
|
|
for _, p := range k.Params {
|
|
if p.Name == "whiteboard_id" && p.Required {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("EventKey %s must declare required whiteboard_id param", eventTypeWhiteboardUpdated)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("EventKey %s not registered", eventTypeWhiteboardUpdated)
|
|
}
|
|
|
|
// 确保 event.APIClient 接口与本测试 mock 一致。
|
|
var _ event.APIClient = (*fakeAPIClient)(nil)
|