Files
dingtalk-workspace-cli/internal/helpers/devapp_robot_connect_test.go

174 lines
6.4 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 helpers
import (
"bytes"
"context"
"strings"
"testing"
)
// TestDevAppRobotConnectRegistered confirms `dev connect` is wired into
// the robot subtree alongside the (reused) provisioning commands.
func TestDevAppRobotConnectRegistered(t *testing.T) {
root := newDevAppCommand(&captureRunner{})
if _, _, err := root.Find([]string{"robot", "connect"}); err != nil {
t.Fatalf("dev connect not registered: %v", err)
}
// Provisioning stays under robot (reused, not duplicated by connect).
for _, name := range []string{"create", "submit", "result"} {
if _, _, err := root.Find([]string{"robot", name}); err != nil {
t.Fatalf("devapp robot %s missing: %v", name, err)
}
}
}
// TestDevAppRobotConnectValidation covers the credential / channel guards on the
// dry-run path (which never launches the Stream connector).
func TestDevAppRobotConnectValidation(t *testing.T) {
cases := []struct {
name string
args []string
wantErr string
wantJSON []string // substrings expected in successful dry-run output
wantStderr []string // substrings expected in combined stderr/stdout stream
notStderr []string // substrings that must NOT appear
}{
{
name: "no credentials and no unified-app-id",
args: []string{"--channel", "claudecode", "--dry-run"},
wantErr: "需要 --robot-client-id/--robot-client-secret",
},
{
name: "unknown channel",
args: []string{"--channel", "nope", "--robot-client-id", "a", "--robot-client-secret", "b", "--dry-run"},
wantErr: "未知渠道",
},
{
name: "explicit credentials dry-run emits plan and secret warning",
args: []string{"--channel", "claudecode", "--robot-client-id", "id1", "--robot-client-secret", "sec1", "--dry-run"},
wantJSON: []string{"\"credentialSource\"", "flag:--robot-client-id/--robot-client-secret", "stream-bridge", "\"clientId\"", "\"completionState\": \"LOCAL_DEBUG_ONLY\"", "\"doesNotPublish\": true", "\"scope\": \"local_debug_only\""},
wantStderr: []string{"[connect] WARNING", "--robot-client-secret 出现在命令行", "建议改用 --unified-app-id"},
},
{
name: "unified-app-id dry-run skips credentials get and emits no secret warning",
args: []string{"--channel", "qoderwork", "--unified-app-id", "UAID", "--dry-run"},
wantJSON: []string{"credentials get, skipped in dry-run", "\"unifiedAppId\"", "\"completionState\": \"LOCAL_DEBUG_ONLY\"", "\"doesNotPublish\": true", "\"scope\": \"local_debug_only\""},
notStderr: []string{"[connect] WARNING"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
root := newDevAppTestRoot(&captureRunner{})
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs(append([]string{"dev", "connect"}, tc.args...))
err := root.Execute()
if tc.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("want error containing %q, got err=%v out=%s", tc.wantErr, err, out.String())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v\noutput:\n%s", err, out.String())
}
for _, sub := range tc.wantJSON {
if !strings.Contains(out.String(), sub) {
t.Fatalf("output missing %q:\n%s", sub, out.String())
}
}
for _, sub := range tc.wantStderr {
if !strings.Contains(out.String(), sub) {
t.Fatalf("stderr/stdout missing %q:\n%s", sub, out.String())
}
}
for _, sub := range tc.notStderr {
if strings.Contains(out.String(), sub) {
t.Fatalf("stderr/stdout unexpectedly contains %q:\n%s", sub, out.String())
}
}
})
}
}
func TestDevAppRobotConnectLocalDebugNotice(t *testing.T) {
notice := connectLocalDebugNotice()
for _, sub := range []string{"本地调试", "不代表线上发布完成", "不会提交版本发布", "APPROVAL_REQUIRED"} {
if !strings.Contains(notice, sub) {
t.Fatalf("notice = %q, want substring %q", notice, sub)
}
}
}
// TestDevAppFetchCredentials checks that --unified-app-id credential resolution
// reuses get_dev_app_credentials and extracts clientId/clientSecret from the
// MCP envelope.
func TestDevAppFetchCredentials(t *testing.T) {
runner := &devAppResponseRunner{response: map[string]any{
"content": map[string]any{
"result": map[string]any{
"clientId": "ck-123",
"clientSecret": "cs-456",
},
},
}}
root := newDevAppTestRoot(runner)
connectCmd, _, err := root.Find([]string{"dev", "connect"})
if err != nil {
t.Fatalf("find connect: %v", err)
}
connectCmd.SetContext(context.Background())
id, secret, err := devAppFetchCredentials(runner, connectCmd, "UAID-1")
if err != nil {
t.Fatalf("devAppFetchCredentials error: %v", err)
}
if id != "ck-123" || secret != "cs-456" {
t.Fatalf("got id=%q secret=%q, want ck-123/cs-456", id, secret)
}
if runner.last.Tool != "get_dev_app_credentials" {
t.Fatalf("Tool = %q, want get_dev_app_credentials", runner.last.Tool)
}
if runner.last.CanonicalProduct != devAppProduct {
t.Fatalf("CanonicalProduct = %q, want %q", runner.last.CanonicalProduct, devAppProduct)
}
if got := runner.last.Params["unifiedAppId"]; got != "UAID-1" {
t.Fatalf("unifiedAppId param = %v, want UAID-1", got)
}
}
// TestDevAppConnectUnwrap covers the envelope descent and the appKey/appSecret
// field fallbacks.
func TestDevAppConnectUnwrap(t *testing.T) {
// appKey/appSecret fallback when clientId/clientSecret absent.
payload := devAppConnectUnwrap(map[string]any{
"result": map[string]any{"appKey": "ak", "appSecret": "as"},
})
if got := devAppConnectFirst(payload, "clientId", "appKey"); got != "ak" {
t.Fatalf("clientId fallback = %q, want ak", got)
}
if got := devAppConnectFirst(payload, "clientSecret", "appSecret"); got != "as" {
t.Fatalf("clientSecret fallback = %q, want as", got)
}
// nil / missing tolerated.
if got := devAppConnectFirst(nil, "clientId"); got != "" {
t.Fatalf("nil map = %q, want empty", got)
}
}