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

159 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
"encoding/json"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/spf13/cobra"
)
func newPipelineLeafCmd(format string) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
cmd := &cobra.Command{Use: "leaf"}
cmd.Flags().String("format", format, "")
var out, errOut bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&errOut)
return cmd, &out, &errOut
}
// runDevAppFamilyProdAligned 是 B118/B119/B120 共用的生产对齐 runner:
// 在 newDevAppTestRoot 基础上补齐生产根命令缺的全局 --jq/--fields flag 与
// SilenceUsage=true(对齐 internal/app/root.go bindPersistentFlags + 根命令
// SilenceUsage),并注入--json 简写布尔。这样 --jq/--fields 可用、错误路径
// 不把 usage 打到 stdout(管道安全断言的前提)。
func runDevAppFamilyProdAligned(t *testing.T, runner executor.Runner, args ...string) (*bytes.Buffer, *bytes.Buffer, error) {
t.Helper()
// 复用与生产一致的 ResultStore/PostRun emitter,再补齐其余全局 flag。
root := newDevAppTestRoot(runner)
root.SilenceUsage = true
root.SilenceErrors = true
root.PersistentFlags().String("fields", "", "筛选输出字段 (逗号分隔, 如: name,id,status)")
root.PersistentFlags().String("jq", "", "jq 表达式过滤输出 (如: '.items[] | .name')")
root.PersistentFlags().Bool("json", false, "以 JSON 输出")
return runRootBuffered(t, root, args...)
}
// runRootBuffered 在 stdout/stderr 分流下执行任意 root,返回两路缓冲与错误。
func runRootBuffered(t *testing.T, root *cobra.Command, args ...string) (*bytes.Buffer, *bytes.Buffer, error) {
t.Helper()
var out, errBuf bytes.Buffer
root.SetOut(&out)
root.SetErr(&errBuf)
root.SetIn(strings.NewReader(""))
root.SetArgs(args)
err := root.Execute()
return &out, &errBuf, err
}
// TestDevPipelineSafetyStdoutJSONParseable 是队列 B118 的管道安全断言(成功
// 路径):stdout 必须恰为一个可 JSON 解析的完整信封,且默认 json 输出与
// --jq 提取同一字段等价(jq 等价断言)。这保证 `dws ... | jq .` 管道不会因
// 日志/人读杂音混入 stdout 而解析失败。
func TestDevPipelineSafetyStdoutJSONParseable(t *testing.T) {
content := map[string]any{
"unifiedAppId": "u-1",
"name": "DemoApp",
"appStatus": "ENABLED",
}
// 成功叶:stdout 恰为一个 JSON 信封(无日志字节污染)。
out, errBuf, err := runDevAppFamily(t, devAppFamilyContentRunner(content),
"dev", "app", "get", "--unified-app-id", "u-1")
if err != nil {
t.Fatalf("Execute() error = %v\nstdout:\n%s\nstderr:\n%s", err, out.String(), errBuf.String())
}
var env map[string]any
if err := json.Unmarshal(out.Bytes(), &env); err != nil {
t.Fatalf("stdout is not parseable as a single JSON envelope: %v\n%s", err, out.String())
}
if env["ok"] != true || env["outcome"] != "success" {
t.Fatalf("envelope = %#v", env)
}
// jq 等价:--jq .data.name 必须与信封 data.name 一致。
jqOut, jqErr, err := runDevAppFamilyProdAligned(t, devAppFamilyContentRunner(content),
"dev", "app", "get", "--unified-app-id", "u-1", "--jq", ".data.name")
if err != nil {
t.Fatalf("Execute(--jq) error = %v\nstdout:\n%s\nstderr:\n%s", err, jqOut.String(), jqErr.String())
}
if strings.TrimSpace(jqOut.String()) != `"DemoApp"` {
t.Fatalf("--jq .data.name = %q, want \"DemoApp\"", jqOut.String())
}
}
// TestDevPipelineSafetyFailureStdoutZeroBytes 是队列 B118 的失败路径管道安全
// 断言:参数校验失败错误走 apperrors 通道(不进信封),stdout 必须严格零
// 字节——`dws ... | jq .` 在失败时不应把错误细节混进 stdout 数据流。
// (失败细节由生产 Execute 的 printExecutionError 落 stderr——本测试聚焦
// stdout 数据通道纯净性,与轮11-B0 族错误路径口径一致。)
func TestDevPipelineSafetyFailureStdoutZeroBytes(t *testing.T) {
out, _, err := runDevAppFamilyProdAligned(t, &captureRunner{},
"dev", "app", "get") // 缺 --unified-app-id / --app-key
if err == nil || !strings.Contains(err.Error(), "请传入 --unified-app-id 或 --app-key") {
t.Fatalf("Execute() error = %v, want locator validation", err)
}
if out.Len() != 0 {
t.Fatalf("validation failure must keep stdout zero bytes (pipeline safety): got %q", out.String())
}
}
// TestDevPipelineSafetyFailureEnvelopeJSONOnStderr 是队列 B118 的失败信封
// 管道安全断言:显式信封装失败(如 Confirmation 门禁)经错误通道落 stderr 且
// 为完整 JSON 信封,stdout 零字节。错误信封不得被 jq/format 过滤为空。
func TestDevPipelineSafetyFailureEnvelopeJSONOnStderr(t *testing.T) {
// 写叶子真实执行(无 --dry-run、无 --yes)触发 confirmation_required 门禁。
// 信封经 writeEnvelope → Emitter 落 stderr(stdout 严格零字节)。
out, _, err := runDevAppFamilyProdAligned(t,
devAppFamilyContentRunner(map[string]any{"unifiedAppId": "u-1", "name": "x"}),
"dev", "app", "update", "--unified-app-id", "u-1", "--name", "x")
if err == nil {
t.Fatalf("update without --yes must fail, got nil")
}
if out.Len() != 0 {
t.Fatalf("confirmation failure must keep stdout zero bytes, got %q", out.String())
}
// 错误路径完整 JSON 信封落在 stderr(结构化错误通道)。面板级观察:
// 此处直接复用 writeEnvelope 的 Emitter 语义做单测视角——先验证
// writeEnvelope 对失败信封的 stderr 输出为 JSON,再端到端断言 stdout 零字节。
cmd, cmdOut, cmdErr := newPipelineLeafCmd("json")
cmd.SilenceUsage = true
cmd.SetOut(cmdOut)
cmd.SetErr(cmdErr)
failEnv := output.NewFailureEnvelope(&output.ErrorInfo{Type: "permission", Subtype: "confirmation_required", Message: "need confirmation"})
if werr := writeEnvelope(cmd, failEnv); werr != nil {
t.Fatalf("writeEnvelope failure envelope error = %v", werr)
}
if cmdOut.Len() != 0 {
t.Fatalf("writeEnvelope failure must keep stdout zero bytes, got %q", cmdOut.String())
}
var env struct {
OK bool `json:"ok"`
Outcome string `json:"outcome"`
}
if jerr := json.Unmarshal(cmdErr.Bytes(), &env); jerr != nil {
t.Fatalf("stderr is not a JSON envelope: %v\n%s", jerr, cmdErr.String())
}
if env.OK || env.Outcome != "failure" {
t.Fatalf("stderr envelope ok/outcome = %v/%q, want false/failure", env.OK, env.Outcome)
}
}
var _ = output.FormatJSON