758 lines
25 KiB
Go
758 lines
25 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/personal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestPersonalEventListHidesSchemaIDs(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{name: "table", args: []string{"--as", "user"}},
|
|
{name: "json", args: []string{"--as", "user", "--format", "json"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cmd := newEventListCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs(tc.args)
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
got := out.String()
|
|
assertPersonalOutputHidesSchemaIDs(t, got)
|
|
for _, eventKey := range []string{
|
|
personal.EventFromUser,
|
|
personal.EventAllSingleChat,
|
|
personal.EventAllGroupChat,
|
|
personal.EventReadO2O,
|
|
personal.EventReadGroup,
|
|
personal.EventRecallO2O,
|
|
personal.EventRecallGroup,
|
|
personal.EventReactionO2O,
|
|
personal.EventReactionGroup,
|
|
personal.EventGroupUpdated,
|
|
personal.EventGroupMemberAdded,
|
|
personal.EventGroupMemberExited,
|
|
personal.EventGroupDisbanded,
|
|
} {
|
|
if !strings.Contains(got, eventKey) {
|
|
t.Fatalf("list output missing %s: %s", eventKey, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEventListDefaultsToUser(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
cmd := newEventListCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
got := out.String()
|
|
if !strings.Contains(got, personal.EventSingleChat) || !strings.Contains(got, "EVENT_KEY") {
|
|
t.Fatalf("list output = %s, want personal event catalog", got)
|
|
}
|
|
if !strings.Contains(got, personal.EventFromUser) {
|
|
t.Fatalf("list output missing public event %s: %s", personal.EventFromUser, got)
|
|
}
|
|
if strings.Contains(got, "CLIENT_ID") || strings.Contains(got, "ClientSecret") {
|
|
t.Fatalf("list default appears to use legacy application output: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestEventPublicHelpHidesAppMode(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
cmd *cobra.Command
|
|
}{
|
|
{name: "consume", cmd: newEventConsumeCommand()},
|
|
{name: "list", cmd: newEventListCommand()},
|
|
{name: "schema", cmd: newEventSchemaCommand()},
|
|
{name: "status", cmd: newEventStatusCommand()},
|
|
{name: "stop", cmd: newEventStopCommand()},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var out bytes.Buffer
|
|
tc.cmd.SetOut(&out)
|
|
tc.cmd.SetArgs([]string{"--help"})
|
|
if tc.name == "schema" {
|
|
tc.cmd.SetArgs([]string{personal.EventSingleChat, "--help"})
|
|
}
|
|
if err := tc.cmd.Execute(); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
got := out.String()
|
|
for _, hidden := range []string{"--as", "user|app", "应用事件" + " Stream"} {
|
|
if strings.Contains(got, hidden) {
|
|
t.Fatalf("%s help leaked %q:\n%s", tc.name, hidden, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEventListAppOnlyFlagsRejectedForPersonalEvents(t *testing.T) {
|
|
cmd := newEventListCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs([]string{"--all"})
|
|
err := cmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "--all are not supported for personal events") {
|
|
t.Fatalf("Execute() error = %v, want unsupported flag validation", err)
|
|
}
|
|
}
|
|
|
|
func TestEventAsAppRejected(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
for _, cmd := range []*cobra.Command{
|
|
newEventListCommand(),
|
|
newEventStatusCommand(),
|
|
newEventConsumeCommand(),
|
|
newEventStopCommand(),
|
|
newEventSchemaCommand(),
|
|
} {
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs([]string{"--as", "app"})
|
|
if cmd.Use == "schema <event_key>" {
|
|
cmd.SetArgs([]string{personal.EventSingleChat, "--as", "app"})
|
|
}
|
|
err := cmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "app event is not publicly available yet") {
|
|
t.Fatalf("%s Execute() error = %v, want public availability guard", cmd.Use, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEventStatusAppOnlyFlagsRejectedForPersonalEvents(t *testing.T) {
|
|
cmd := newEventStatusCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs([]string{"--all", "--fail-on-orphan"})
|
|
err := cmd.Execute()
|
|
if err == nil ||
|
|
!strings.Contains(err.Error(), "--all") ||
|
|
!strings.Contains(err.Error(), "--fail-on-orphan") ||
|
|
!strings.Contains(err.Error(), "not supported for personal events") {
|
|
t.Fatalf("Execute() error = %v, want unsupported flag validation", err)
|
|
}
|
|
}
|
|
|
|
func TestPersonalEventSchemaHidesSchemaIDs(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{name: "default", args: []string{personal.EventSingleChat, "--as", "user"}},
|
|
{name: "json", args: []string{personal.EventSingleChat, "--as", "user", "--format", "json"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs(tc.args)
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
assertPersonalOutputHidesSchemaIDs(t, out.String())
|
|
if strings.Contains(out.String(), "Schemas") {
|
|
t.Fatalf("schema output contains Schemas line: %s", out.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPersonalEventSchemaDefaultsToTransportEnvelope(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs([]string{personal.EventSingleChat})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output is not JSON: %v\n%s", err, out.String())
|
|
}
|
|
if doc["jq_root_path"] != ".data | fromjson" {
|
|
t.Fatalf("jq_root_path = %#v, want .data | fromjson", doc["jq_root_path"])
|
|
}
|
|
schema, ok := doc["schema"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema = %#v, want object", doc["schema"])
|
|
}
|
|
props, ok := schema["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema.properties = %#v, want object", schema["properties"])
|
|
}
|
|
for _, field := range []string{"type", "seq", "event_type", "data", "headers", "subscribe_id"} {
|
|
if _, ok := props[field]; !ok {
|
|
t.Fatalf("default envelope schema missing %q: %#v", field, props)
|
|
}
|
|
}
|
|
for _, field := range []string{"content", "sender", "conversation_id", "timestamp"} {
|
|
if _, ok := props[field]; ok {
|
|
t.Fatalf("default envelope schema unexpectedly contains flat field %q", field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPersonalEventFlattenedSchemaUsesSingleJSONSchema(t *testing.T) {
|
|
for _, eventKey := range []string{
|
|
personal.EventMention,
|
|
personal.EventSingleChat,
|
|
personal.EventInChat,
|
|
personal.EventAllSingleChat,
|
|
personal.EventAllGroupChat,
|
|
} {
|
|
t.Run(eventKey, func(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs([]string{eventKey, "--flatten"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
got := out.String()
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output for %s is not JSON: %v\n%s", eventKey, err, got)
|
|
}
|
|
for _, want := range []string{
|
|
"event_key",
|
|
"display_name",
|
|
"description",
|
|
"category",
|
|
"rule_type",
|
|
"required_params",
|
|
"jq_root_path",
|
|
"schema",
|
|
"event_id",
|
|
"timestamp",
|
|
"subscribe_id",
|
|
"content",
|
|
"sender",
|
|
"sender_open_dingtalk_id",
|
|
"conversation_id",
|
|
"message_id",
|
|
"create_time",
|
|
"event_time",
|
|
"quoted_message",
|
|
"forward_messages",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("schema output for %s missing %q: %s", eventKey, want, got)
|
|
}
|
|
}
|
|
for _, leaked := range []string{
|
|
"message.text",
|
|
"chat.openConversationId",
|
|
"sender.userId",
|
|
"sender.unionId",
|
|
"auth",
|
|
"resolved_output_schema",
|
|
"decoded_data_schema",
|
|
"filter_schema",
|
|
"payload_schema",
|
|
"output_schema",
|
|
"data_json_path",
|
|
"headers",
|
|
"audit",
|
|
"tenant",
|
|
"subject",
|
|
"traceId",
|
|
"msgIdMetaq",
|
|
"at_users",
|
|
"sender_user_id",
|
|
} {
|
|
if strings.Contains(got, leaked) {
|
|
t.Fatalf("schema output for %s leaked %q: %s", eventKey, leaked, got)
|
|
}
|
|
}
|
|
if doc["jq_root_path"] != "." {
|
|
t.Fatalf("jq_root_path = %#v, want .", doc["jq_root_path"])
|
|
}
|
|
schema, ok := doc["schema"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema = %#v, want object", doc["schema"])
|
|
}
|
|
props, ok := schema["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema.properties = %#v, want object", schema["properties"])
|
|
}
|
|
if _, ok := props["content"].(map[string]any); !ok {
|
|
t.Fatalf("schema.properties.content = %#v, want object", props["content"])
|
|
}
|
|
quoted, ok := props["quoted_message"].(map[string]any)
|
|
if !ok || quoted["type"] != "object" {
|
|
t.Fatalf("schema.properties.quoted_message = %#v, want object", props["quoted_message"])
|
|
}
|
|
forward, ok := props["forward_messages"].(map[string]any)
|
|
if !ok || forward["type"] != "array" {
|
|
t.Fatalf("schema.properties.forward_messages = %#v, want array", props["forward_messages"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPersonalGroupLifecycleEventSchemaUsesConservativePayload(t *testing.T) {
|
|
for _, eventKey := range []string{personal.EventGroupUpdated, personal.EventGroupDisbanded} {
|
|
t.Run(eventKey, func(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs([]string{eventKey, "--flatten"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output is not JSON: %v\n%s", err, out.String())
|
|
}
|
|
if doc["event_key"] != eventKey || doc["rule_type"] != "group" || doc["jq_root_path"] != "." {
|
|
t.Fatalf("schema metadata = %#v", doc)
|
|
}
|
|
required, ok := doc["required_params"].([]any)
|
|
if !ok || len(required) != 1 || required[0] != "group" {
|
|
t.Fatalf("required_params = %#v, want [group]", doc["required_params"])
|
|
}
|
|
schema := doc["schema"].(map[string]any)
|
|
properties := schema["properties"].(map[string]any)
|
|
if len(properties) != 5 {
|
|
t.Fatalf("schema.properties = %#v, want five conservative fields", properties)
|
|
}
|
|
payload, ok := properties["payload"].(map[string]any)
|
|
if !ok || payload["type"] != "object" || payload["additionalProperties"] != true {
|
|
t.Fatalf("schema.properties.payload = %#v", properties["payload"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPersonalGroupMemberEventSchemaMatchesFlatOutput(t *testing.T) {
|
|
wantProperties := []string{
|
|
"type", "event_id", "timestamp", "subscribe_id", "conversation_id",
|
|
"operator", "operator_open_dingtalk_id", "members", "event_time",
|
|
}
|
|
for _, eventKey := range []string{personal.EventGroupMemberAdded, personal.EventGroupMemberExited} {
|
|
t.Run(eventKey, func(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs([]string{eventKey, "--flatten"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output is not JSON: %v\n%s", err, out.String())
|
|
}
|
|
if doc["event_key"] != eventKey || doc["rule_type"] != "group" || doc["jq_root_path"] != "." {
|
|
t.Fatalf("schema metadata = %#v", doc)
|
|
}
|
|
properties := doc["schema"].(map[string]any)["properties"].(map[string]any)
|
|
if len(properties) != len(wantProperties) {
|
|
t.Fatalf("schema.properties = %#v, want exactly %d flat fields", properties, len(wantProperties))
|
|
}
|
|
for _, field := range wantProperties {
|
|
if _, ok := properties[field].(map[string]any); !ok {
|
|
t.Fatalf("schema missing %q: %#v", field, properties)
|
|
}
|
|
}
|
|
members := properties["members"].(map[string]any)
|
|
items, ok := members["items"].(map[string]any)
|
|
if !ok || members["type"] != "array" || items["type"] != "object" {
|
|
t.Fatalf("members schema = %#v", members)
|
|
}
|
|
memberProperties, ok := items["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("members.items.properties = %#v", items["properties"])
|
|
}
|
|
for _, field := range []string{"nick", "open_dingtalk_id"} {
|
|
if _, ok := memberProperties[field].(map[string]any); !ok {
|
|
t.Fatalf("member schema missing %q: %#v", field, memberProperties)
|
|
}
|
|
}
|
|
if _, ok := properties["payload"]; ok {
|
|
t.Fatalf("group member schema exposed generic payload: %#v", properties)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPersonalActionEventSchemaMatchesFlatOutput(t *testing.T) {
|
|
tests := []struct {
|
|
eventKeys []string
|
|
properties []string
|
|
}{
|
|
{
|
|
eventKeys: []string{personal.EventReadO2O, personal.EventReadGroup},
|
|
properties: []string{
|
|
"type", "event_id", "timestamp", "subscribe_id", "message_id",
|
|
"conversation_id", "reader", "reader_open_dingtalk_id", "sender",
|
|
"sender_open_dingtalk_id", "read_time", "event_time",
|
|
},
|
|
},
|
|
{
|
|
eventKeys: []string{personal.EventRecallO2O, personal.EventRecallGroup},
|
|
properties: []string{
|
|
"type", "event_id", "timestamp", "subscribe_id", "message_id",
|
|
"conversation_id", "recaller", "recaller_open_dingtalk_id", "sender",
|
|
"sender_open_dingtalk_id", "recall_time", "event_time",
|
|
},
|
|
},
|
|
{
|
|
eventKeys: []string{personal.EventReactionO2O, personal.EventReactionGroup},
|
|
properties: []string{
|
|
"type", "event_id", "timestamp", "subscribe_id", "message_id",
|
|
"conversation_id", "operator", "operator_open_dingtalk_id", "reaction_name",
|
|
"reaction_text", "operation_type", "operation_time", "sender",
|
|
"sender_open_dingtalk_id", "event_time",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, eventKey := range tt.eventKeys {
|
|
t.Run(eventKey, func(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs([]string{eventKey, "--flatten"})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output is not JSON: %v\n%s", err, out.String())
|
|
}
|
|
if doc["event_key"] != eventKey || doc["jq_root_path"] != "." {
|
|
t.Fatalf("schema metadata = %#v", doc)
|
|
}
|
|
schema, ok := doc["schema"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema = %#v", doc["schema"])
|
|
}
|
|
properties, ok := schema["properties"].(map[string]any)
|
|
if !ok || len(properties) != len(tt.properties) {
|
|
t.Fatalf("schema.properties = %#v, want exactly %d flat fields", schema["properties"], len(tt.properties))
|
|
}
|
|
for _, field := range tt.properties {
|
|
if _, ok := properties[field]; !ok {
|
|
t.Fatalf("schema missing %q: %#v", field, properties)
|
|
}
|
|
}
|
|
for _, internal := range []string{"payload", "uid", "corpid", "clientId", "filterSubId", "bizid"} {
|
|
if _, ok := properties[internal]; ok {
|
|
t.Fatalf("schema exposed internal property %q", internal)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEventSchemaDefaultsToUser(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetArgs([]string{personal.EventSingleChat})
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output is not JSON: %v\n%s", err, out.String())
|
|
}
|
|
if doc["event_key"] != personal.EventSingleChat {
|
|
t.Fatalf("event_key = %#v, want %s", doc["event_key"], personal.EventSingleChat)
|
|
}
|
|
if doc["jq_root_path"] != ".data | fromjson" {
|
|
t.Fatalf("jq_root_path = %#v, want default envelope path", doc["jq_root_path"])
|
|
}
|
|
}
|
|
|
|
func TestPersonalEventFromUserIsPubliclyAvailable(t *testing.T) {
|
|
if err := ensurePublicPersonalEvent(personal.EventFromUser); err != nil {
|
|
t.Fatalf("ensurePublicPersonalEvent() error = %v", err)
|
|
}
|
|
|
|
schemaCmd := newEventSchemaCommand()
|
|
schemaCmd.SilenceUsage = true
|
|
schemaCmd.SilenceErrors = true
|
|
var schemaOut bytes.Buffer
|
|
schemaCmd.SetOut(&schemaOut)
|
|
schemaCmd.SetArgs([]string{personal.EventFromUser})
|
|
if err := schemaCmd.Execute(); err != nil {
|
|
t.Fatalf("schema Execute() error = %v", err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(schemaOut.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output is not JSON: %v\n%s", err, schemaOut.String())
|
|
}
|
|
if doc["event_key"] != personal.EventFromUser || doc["rule_type"] != "sender" {
|
|
t.Fatalf("schema document = %#v", doc)
|
|
}
|
|
|
|
configDir := setupPersonalIdentityToken(t, &authpkg.TokenData{
|
|
AccessToken: "access-1",
|
|
RefreshToken: "refresh-1",
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
RefreshExpAt: time.Now().Add(24 * time.Hour),
|
|
CorpID: "corp-1",
|
|
UserID: "user-1",
|
|
ClientID: "client-1",
|
|
})
|
|
t.Setenv("DWS_CONFIG_DIR", configDir)
|
|
|
|
consumeCmd := newEventConsumeCommand()
|
|
consumeCmd.SilenceUsage = true
|
|
consumeCmd.SilenceErrors = true
|
|
consumeCmd.SetArgs([]string{personal.EventFromUser, "--user", "test-user-001", "--dry-run"})
|
|
if err := consumeCmd.Execute(); err != nil {
|
|
t.Fatalf("consume dry-run Execute() error = %v", err)
|
|
}
|
|
openIDConsumeCmd := newEventConsumeCommand()
|
|
openIDConsumeCmd.SilenceUsage = true
|
|
openIDConsumeCmd.SilenceErrors = true
|
|
openIDConsumeCmd.SetArgs([]string{personal.EventFromUser, "--open-dingtalk-id", "open-user-1", "--dry-run"})
|
|
if err := openIDConsumeCmd.Execute(); err != nil {
|
|
t.Fatalf("consume openDingtalkId dry-run Execute() error = %v", err)
|
|
}
|
|
|
|
conflictingTargetCmd := newEventConsumeCommand()
|
|
conflictingTargetCmd.SilenceUsage = true
|
|
conflictingTargetCmd.SilenceErrors = true
|
|
conflictingTargetCmd.SetArgs([]string{personal.EventFromUser, "--user", "test-user-001", "--open-dingtalk-id", "open-user-1", "--dry-run"})
|
|
err := conflictingTargetCmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "--user and --open-dingtalk-id are mutually exclusive for "+personal.EventFromUser) {
|
|
t.Fatalf("conflicting target identity error = %v", err)
|
|
}
|
|
|
|
groupOpenIDCmd := newEventConsumeCommand()
|
|
groupOpenIDCmd.SilenceUsage = true
|
|
groupOpenIDCmd.SilenceErrors = true
|
|
groupOpenIDCmd.SetArgs([]string{personal.EventInChat, "--group", "cid-1", "--open-dingtalk-id", "open-user-1", "--dry-run"})
|
|
err = groupOpenIDCmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "--open-dingtalk-id is not supported for "+personal.EventInChat+"; use --group") {
|
|
t.Fatalf("group openDingtalkId error = %v", err)
|
|
}
|
|
|
|
missingUserCmd := newEventConsumeCommand()
|
|
missingUserCmd.SilenceUsage = true
|
|
missingUserCmd.SilenceErrors = true
|
|
missingUserCmd.SetArgs([]string{personal.EventFromUser})
|
|
err = missingUserCmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "one of --user or --open-dingtalk-id is required for "+personal.EventFromUser) {
|
|
t.Fatalf("missing target identity error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPersonalNewIMEventsDryRunAndValidation(t *testing.T) {
|
|
configDir := setupPersonalIdentityToken(t, &authpkg.TokenData{
|
|
AccessToken: "access-1",
|
|
RefreshToken: "refresh-1",
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
RefreshExpAt: time.Now().Add(24 * time.Hour),
|
|
CorpID: "corp-1",
|
|
UserID: "user-1",
|
|
ClientID: "client-1",
|
|
})
|
|
t.Setenv("DWS_CONFIG_DIR", configDir)
|
|
|
|
for _, test := range []struct {
|
|
eventKey string
|
|
args []string
|
|
}{
|
|
{eventKey: personal.EventAllSingleChat},
|
|
{eventKey: personal.EventAllGroupChat},
|
|
{eventKey: personal.EventGroupUpdated, args: []string{"--group", "cid-test-group"}},
|
|
{eventKey: personal.EventGroupMemberAdded, args: []string{"--group", "cid-test-group"}},
|
|
{eventKey: personal.EventGroupMemberExited, args: []string{"--group", "cid-test-group"}},
|
|
{eventKey: personal.EventGroupDisbanded, args: []string{"--group", "cid-test-group"}},
|
|
} {
|
|
t.Run(test.eventKey, func(t *testing.T) {
|
|
if err := ensurePublicPersonalEvent(test.eventKey); err != nil {
|
|
t.Fatalf("ensurePublicPersonalEvent() error = %v", err)
|
|
}
|
|
cmd := newEventConsumeCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs(append([]string{test.eventKey}, append(test.args, "--dry-run")...))
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("dry-run Execute() error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, eventKey := range []string{personal.EventAllSingleChat, personal.EventAllGroupChat} {
|
|
cmd := newEventConsumeCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs([]string{eventKey, "--user", "test-user-001", "--dry-run"})
|
|
if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "--user is not supported for "+eventKey) {
|
|
t.Fatalf("%s scoped user error = %v", eventKey, err)
|
|
}
|
|
}
|
|
|
|
for _, eventKey := range []string{personal.EventGroupUpdated, personal.EventGroupMemberAdded, personal.EventGroupMemberExited, personal.EventGroupDisbanded} {
|
|
cmd := newEventConsumeCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs([]string{eventKey, "--dry-run"})
|
|
if err := cmd.Execute(); err == nil || !strings.Contains(err.Error(), "--group is required for "+eventKey) {
|
|
t.Fatalf("%s missing group error = %v", eventKey, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEventConsumeCobraSchemaIncludesOpenDingTalkID(t *testing.T) {
|
|
root := NewRootCommand()
|
|
root.SilenceUsage = true
|
|
root.SilenceErrors = true
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetArgs([]string{"schema", "event consume"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("schema event consume Execute() error = %v", err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
|
t.Fatalf("schema output is not JSON: %v\n%s", err, out.String())
|
|
}
|
|
params, ok := doc["parameters"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema parameters = %#v", doc["parameters"])
|
|
}
|
|
if _, ok := params["open-dingtalk-id"]; !ok {
|
|
t.Fatalf("schema parameters missing open-dingtalk-id: %#v", params)
|
|
}
|
|
if _, ok := params["odid"]; ok {
|
|
t.Fatalf("schema parameters unexpectedly include odid alias: %#v", params)
|
|
}
|
|
if _, ok := params["flatten"]; !ok {
|
|
t.Fatalf("schema parameters missing flatten: %#v", params)
|
|
}
|
|
for _, name := range []string{"user", "open-dingtalk-id", "group"} {
|
|
param, ok := params[name].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema parameter %s = %#v", name, params[name])
|
|
}
|
|
if got, exists := param["required_when"]; exists {
|
|
t.Fatalf("schema parameter %s unexpectedly declares required_when = %#v", name, got)
|
|
}
|
|
}
|
|
constraints, ok := doc["constraints"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema constraints = %#v", doc["constraints"])
|
|
}
|
|
assertJSONConstraintGroup := func(field string, want []string) {
|
|
t.Helper()
|
|
groups, ok := constraints[field].([]any)
|
|
if !ok {
|
|
t.Fatalf("schema constraint %s = %#v", field, constraints[field])
|
|
}
|
|
for _, rawGroup := range groups {
|
|
group, ok := rawGroup.([]any)
|
|
if !ok || len(group) != len(want) {
|
|
continue
|
|
}
|
|
matched := true
|
|
for i := range want {
|
|
if group[i] != want[i] {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if matched {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("schema constraint %s = %#v, missing %#v", field, groups, want)
|
|
}
|
|
assertJSONConstraintGroup("require_one_of", []string{"event_key", "subscribe-id"})
|
|
positionals, ok := doc["positionals"].([]any)
|
|
if !ok || len(positionals) != 1 {
|
|
t.Fatalf("schema positionals = %#v", doc["positionals"])
|
|
}
|
|
eventKey, ok := positionals[0].(map[string]any)
|
|
if !ok || eventKey["name"] != "event_key" || eventKey["variadic"] != true {
|
|
t.Fatalf("event_key positional = %#v, want variadic", positionals[0])
|
|
}
|
|
}
|
|
|
|
func TestPersonalEventSchemaRejectsTableFormat(t *testing.T) {
|
|
cmd := newEventSchemaCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs([]string{personal.EventSingleChat, "--format", "table"})
|
|
err := cmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "event schema only supports json output") {
|
|
t.Fatalf("Execute() error = %v, want json-only format validation", err)
|
|
}
|
|
}
|
|
|
|
func TestEventAsBotRejected(t *testing.T) {
|
|
cmd := newEventListCommand()
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
cmd.SetArgs([]string{"--as", "bot"})
|
|
err := cmd.Execute()
|
|
if err == nil || !strings.Contains(err.Error(), "app event is not publicly available yet") {
|
|
t.Fatalf("Execute() error = %v, want public availability guard", err)
|
|
}
|
|
}
|
|
|
|
func assertPersonalOutputHidesSchemaIDs(t *testing.T, out string) {
|
|
t.Helper()
|
|
for _, leaked := range []string{"SCHEMA_IDS", "schema_ids", "im_msg_23", "im_msg_29"} {
|
|
if strings.Contains(out, leaked) {
|
|
t.Fatalf("output leaked %q: %s", leaked, out)
|
|
}
|
|
}
|
|
}
|