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
302 lines
11 KiB
Go
302 lines
11 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package calendar
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// newAttendeeValidateRuntime builds a RuntimeContext with the add/remove
|
|
// attendee-id flags set, for exercising validateCalendarUpdateAttendees.
|
|
func newAttendeeValidateRuntime(t *testing.T, add, remove string) *common.RuntimeContext {
|
|
t.Helper()
|
|
cmd := &cobra.Command{Use: "test"}
|
|
cmd.Flags().String("add-attendee-ids", "", "")
|
|
cmd.Flags().String("remove-attendee-ids", "", "")
|
|
if err := cmd.ParseFlags(nil); err != nil {
|
|
t.Fatalf("ParseFlags: %v", err)
|
|
}
|
|
if add != "" {
|
|
_ = cmd.Flags().Set("add-attendee-ids", add)
|
|
}
|
|
if remove != "" {
|
|
_ = cmd.Flags().Set("remove-attendee-ids", remove)
|
|
}
|
|
return &common.RuntimeContext{Cmd: cmd}
|
|
}
|
|
|
|
// assertValidationParam asserts err is a *errs.ValidationError whose Param
|
|
// equals wantParam, and returns it for any further message assertions.
|
|
func assertValidationParam(t *testing.T, err error, wantParam string) *errs.ValidationError {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T (%v)", err, err)
|
|
}
|
|
if ve.Param != wantParam {
|
|
t.Errorf("Param = %q, want %q", ve.Param, wantParam)
|
|
}
|
|
return ve
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// withStepContext helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestWithStepContext_Nil(t *testing.T) {
|
|
if got := withStepContext(nil, "step %d", 1); got != nil {
|
|
t.Fatalf("withStepContext(nil) = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestWithStepContext_AppendsToTypedHint(t *testing.T) {
|
|
// A typed error keeps its classification; the context is appended to Hint.
|
|
inner := errs.NewAPIError(errs.SubtypeUnknown, "boom").WithHint("first")
|
|
got := withStepContext(inner, "after steps %v", []string{"event"})
|
|
var ae *errs.APIError
|
|
if !errors.As(got, &ae) {
|
|
t.Fatalf("want *errs.APIError, got %T", got)
|
|
}
|
|
if ae.Hint == "" || !strings.Contains(ae.Hint, "first") || !strings.Contains(ae.Hint, "after steps") {
|
|
t.Errorf("hint should append context, got %q", ae.Hint)
|
|
}
|
|
}
|
|
|
|
func TestWithStepContext_SetsHintWhenEmpty(t *testing.T) {
|
|
inner := errs.NewAPIError(errs.SubtypeUnknown, "boom")
|
|
got := withStepContext(inner, "after steps %v", []string{"event"})
|
|
var ae *errs.APIError
|
|
if !errors.As(got, &ae) {
|
|
t.Fatalf("want *errs.APIError, got %T", got)
|
|
}
|
|
if !strings.Contains(ae.Hint, "after steps") {
|
|
t.Errorf("hint should be set, got %q", ae.Hint)
|
|
}
|
|
}
|
|
|
|
func TestWithStepContext_UnclassifiedFallsBackToInternal(t *testing.T) {
|
|
// A plain, unclassified error is wrapped into a typed internal error so the
|
|
// envelope still tells the truth.
|
|
got := withStepContext(errors.New("raw failure"), "after steps %v", []string{"event"})
|
|
var ie *errs.InternalError
|
|
if !errors.As(got, &ie) {
|
|
t.Fatalf("want *errs.InternalError, got %T", got)
|
|
}
|
|
if ie.Subtype != errs.SubtypeSDKError {
|
|
t.Errorf("subtype=%q, want sdk_error", ie.Subtype)
|
|
}
|
|
if !strings.Contains(ie.Message, "raw failure") {
|
|
t.Errorf("message should preserve original, got %q", ie.Message)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// withParam helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestWithParam_AttachesToValidationError(t *testing.T) {
|
|
inner := errs.NewValidationError(errs.SubtypeInvalidArgument, "boom")
|
|
got := withParam(inner, "--attendee-ids")
|
|
ve := assertValidationParam(t, got, "--attendee-ids")
|
|
if ve != inner {
|
|
t.Errorf("withParam should return the same underlying error, got a different pointer")
|
|
}
|
|
if ve.Message != "boom" {
|
|
t.Errorf("message mutated: got %q, want %q", ve.Message, "boom")
|
|
}
|
|
}
|
|
|
|
func TestWithParam_NonValidationPassesThrough(t *testing.T) {
|
|
inner := errs.NewInternalError(errs.SubtypeSDKError, "io failure")
|
|
got := withParam(inner, "--attendee-ids")
|
|
if got != inner {
|
|
t.Fatalf("non-validation error should pass through unchanged, got %v", got)
|
|
}
|
|
var ve *errs.ValidationError
|
|
if errors.As(got, &ve) {
|
|
t.Fatalf("non-validation error must not become a ValidationError")
|
|
}
|
|
}
|
|
|
|
func TestWithParam_NilPassesThrough(t *testing.T) {
|
|
if got := withParam(nil, "--attendee-ids"); got != nil {
|
|
t.Fatalf("withParam(nil) = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Part A — re-wrap sites: the parseAttendees error, attributed by the caller's
|
|
// flag, must be the inner typed error (not a re-wrapped nesting).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestParseAttendees_AttributedToCreateFlag(t *testing.T) {
|
|
_, err := parseAttendees("bad-id", "")
|
|
// create's add path: withParam(err, "--attendee-ids")
|
|
got := withParam(err, "--attendee-ids")
|
|
assertValidationParam(t, got, "--attendee-ids")
|
|
}
|
|
|
|
func TestParseAttendees_AttributedToAddFlag(t *testing.T) {
|
|
_, err := parseAttendees("bad-id", "")
|
|
// update's add path: withParam(err, "--add-attendee-ids")
|
|
got := withParam(err, "--add-attendee-ids")
|
|
assertValidationParam(t, got, "--add-attendee-ids")
|
|
}
|
|
|
|
func TestParseAttendees_InnerStaysFlagAgnostic(t *testing.T) {
|
|
// The shared inner parser must not pre-attribute a flag; callers do.
|
|
_, err := parseAttendees("bad-id", "")
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
|
}
|
|
if ve.Param != "" {
|
|
t.Errorf("inner parseAttendees should stay flag-agnostic, got Param = %q", ve.Param)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Part B — direct attendee-id format validations carry their flag.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestParseRoomFindAttendees_FormatErrorParam(t *testing.T) {
|
|
_, _, err := parseRoomFindAttendees("bad-id", "")
|
|
assertValidationParam(t, err, "--"+flagAttendees)
|
|
}
|
|
|
|
func TestParseRoomFindAttendees_RejectsRoomID(t *testing.T) {
|
|
// room find only supports ou_/oc_; omm_ rooms are not valid attendees.
|
|
_, _, err := parseRoomFindAttendees("omm_room", "")
|
|
assertValidationParam(t, err, "--"+flagAttendees)
|
|
}
|
|
|
|
func TestParseCalendarAttendeeIDs_StaysFlagAgnostic(t *testing.T) {
|
|
// parseCalendarAttendeeIDs serves BOTH --add-attendee-ids and
|
|
// --remove-attendee-ids, so it must not pre-attribute a flag.
|
|
_, err := parseCalendarAttendeeIDs("bad-id")
|
|
var ve *errs.ValidationError
|
|
if !errors.As(err, &ve) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
|
}
|
|
if ve.Param != "" {
|
|
t.Errorf("shared parser should stay flag-agnostic, got Param = %q", ve.Param)
|
|
}
|
|
}
|
|
|
|
func TestValidateCalendarUpdateAttendees_RemoveFormatParam(t *testing.T) {
|
|
// The remove path attributes its parser error to --remove-attendee-ids.
|
|
rt := newAttendeeValidateRuntime(t, "", "bad-id")
|
|
err := validateCalendarUpdateAttendees(rt)
|
|
assertValidationParam(t, err, "--remove-attendee-ids")
|
|
}
|
|
|
|
func TestValidateCalendarUpdateAttendees_AddFormatParam(t *testing.T) {
|
|
// The add path attributes its parser error to --add-attendee-ids.
|
|
rt := newAttendeeValidateRuntime(t, "bad-id", "")
|
|
err := validateCalendarUpdateAttendees(rt)
|
|
assertValidationParam(t, err, "--add-attendee-ids")
|
|
}
|
|
|
|
// attendeeDeleteIDs's switch default is defensive: parseCalendarAttendeeIDs
|
|
// already rejects any non-ou_/oc_/omm_ id, so only a well-formed id reaches the
|
|
// switch and the valid branches map it. This asserts the happy path maps types.
|
|
func TestAttendeeDeleteIDs_MapsKnownTypes(t *testing.T) {
|
|
got, err := attendeeDeleteIDs("ou_a,oc_b,omm_c")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(got) != 3 {
|
|
t.Fatalf("expected 3 delete ids, got %d: %v", len(got), got)
|
|
}
|
|
wantTypes := map[string]string{"user": "user_id", "chat": "chat_id", "resource": "room_id"}
|
|
for _, m := range got {
|
|
key, ok := wantTypes[m["type"]]
|
|
if !ok {
|
|
t.Errorf("unexpected type %q in %v", m["type"], m)
|
|
continue
|
|
}
|
|
if m[key] == "" {
|
|
t.Errorf("missing %s for type %q in %v", key, m["type"], m)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCalendarAttendeeIDs_Valid(t *testing.T) {
|
|
ids, err := parseCalendarAttendeeIDs(" ou_a , oc_b , ou_a ")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ids) != 2 || ids[0] != "ou_a" || ids[1] != "oc_b" {
|
|
t.Errorf("dedup/trim failed: got %v", ids)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// unwrapCalendarAPIError helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestUnwrapCalendarAPIError_NilReturnsEmpty(t *testing.T) {
|
|
if got := unwrapCalendarAPIError(nil); got != "" {
|
|
t.Errorf("nil err should return empty string, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestUnwrapCalendarAPIError_NonAPIErrorReturnsEmpty(t *testing.T) {
|
|
// Validation, internal, and plain errors are not calendar API business
|
|
// errors; the helper must signal "no specialization" so callers fall back.
|
|
cases := []error{
|
|
errs.NewValidationError(errs.SubtypeInvalidArgument, "bad input"),
|
|
errs.NewInternalError(errs.SubtypeSDKError, "io failure"),
|
|
errors.New("plain error"),
|
|
}
|
|
for _, e := range cases {
|
|
if got := unwrapCalendarAPIError(e); got != "" {
|
|
t.Errorf("unwrapCalendarAPIError(%T) = %q, want empty", e, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnwrapCalendarAPIError_Code190014_ReturnsHint(t *testing.T) {
|
|
ae := errs.NewAPIError(errs.SubtypeInvalidParameters, "invalid params").
|
|
WithCode(190014).
|
|
WithHint("end_time should be later than start_time")
|
|
got := unwrapCalendarAPIError(ae)
|
|
if got != "end_time should be later than start_time" {
|
|
t.Errorf("expected lifted hint, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestUnwrapCalendarAPIError_Code190014_WrappedStillResolves(t *testing.T) {
|
|
// withStepContext wraps the typed error but errors.As must still find it.
|
|
inner := errs.NewAPIError(errs.SubtypeInvalidParameters, "invalid params").
|
|
WithCode(190014).
|
|
WithHint("calendar_id is required")
|
|
wrapped := withStepContext(inner, "while fetching meeting info for %s", "evt_x")
|
|
got := unwrapCalendarAPIError(wrapped)
|
|
if !strings.Contains(got, "calendar_id is required") {
|
|
t.Errorf("expected wrapped 190014 to surface hint, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestUnwrapCalendarAPIError_UnhandledCodeReturnsEmpty(t *testing.T) {
|
|
// An APIError carrying a code that isn't specialized here should return
|
|
// "" so callers fall back to err.Error() — keeps the helper conservative
|
|
// while we add 19xxxx codes incrementally.
|
|
ae := errs.NewAPIError(errs.SubtypeInvalidParameters, "some other error").
|
|
WithCode(190099).
|
|
WithHint("ignore me")
|
|
if got := unwrapCalendarAPIError(ae); got != "" {
|
|
t.Errorf("unhandled code should return empty, got %q", got)
|
|
}
|
|
}
|