259 lines
11 KiB
Go
259 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"yms-daemon/internal/daemonapi"
|
|
)
|
|
|
|
// TestParseUpdateArgsAcceptsBackendAndResolvesFile 验证 update 子命令能接受 backend 服务与 -f 文件参数,
|
|
// 并正确解析出 repack-zip 输入类型与文件路径。
|
|
func TestParseUpdateArgsAcceptsBackendAndResolvesFile(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "package.zip")
|
|
if err := os.WriteFile(file, []byte("zip"), 0o600); err != nil {
|
|
t.Fatalf("write update package: %v", err)
|
|
}
|
|
request, err := parseUpdateArgs([]string{"--service", "backend", "-f", file}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse backend update arguments: %v", err)
|
|
}
|
|
if request.service != "backend" || request.inputType != daemonapi.InputTypeRepackZIP || request.file != file {
|
|
t.Fatalf("unexpected backend update arguments: %+v", request)
|
|
}
|
|
}
|
|
|
|
// TestParseUpdateArgsAcceptsDirectNativeJAR 验证 update 子命令能接受 --native-jar 参数,
|
|
// 并正确解析出 native-jar 输入类型与文件路径。
|
|
func TestParseUpdateArgsAcceptsDirectNativeJAR(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "glory-soft-yms.jar")
|
|
if err := os.WriteFile(file, []byte("jar"), 0o600); err != nil {
|
|
t.Fatalf("write native backend JAR: %v", err)
|
|
}
|
|
request, err := parseUpdateArgs([]string{"--service", "backend", "--native-jar", file}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse direct native backend update arguments: %v", err)
|
|
}
|
|
if request.service != "backend" || request.inputType != daemonapi.InputTypeNativeJAR || request.file != file {
|
|
t.Fatalf("unexpected direct native backend update arguments: %+v", request)
|
|
}
|
|
}
|
|
|
|
// TestParseUpdateArgsAcceptsContainerImage 验证 update 子命令能接受 --container-image 参数,
|
|
// 并正确解析出 container-image 输入类型与镜像引用。
|
|
func TestParseUpdateArgsAcceptsContainerImage(t *testing.T) {
|
|
imageReference := "harbor.ymswell.asia/ymswell/glory-ymswell:20260813-184902-a37bf50d-v1.1.8.1"
|
|
request, err := parseUpdateArgs([]string{"--service", "backend", "--container-image", imageReference}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse container backend update arguments: %v", err)
|
|
}
|
|
if request.service != "backend" || request.inputType != daemonapi.InputTypeContainerImage || request.imageReference != imageReference || request.file != "" {
|
|
t.Fatalf("unexpected container backend update arguments: %+v", request)
|
|
}
|
|
}
|
|
|
|
// TestParseUpdateArgsControlsContainerStartupLogs 验证 --no-start-log 能关闭容器启动日志,
|
|
// 且默认情况下启动日志处于开启状态。
|
|
func TestParseUpdateArgsControlsContainerStartupLogs(t *testing.T) {
|
|
imageReference := "harbor.ymswell.asia/ymswell/glory-ymswell:20260813-184902-a37bf50d-v1.1.8.1"
|
|
request, err := parseUpdateArgs([]string{"--service", "backend", "--container-image", imageReference, "--no-start-log"}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse no-start-log container update arguments: %v", err)
|
|
}
|
|
if request.startLog {
|
|
t.Fatal("--no-start-log did not disable startup logs")
|
|
}
|
|
request, err = parseUpdateArgs([]string{"--service", "backend", "--container-image", imageReference}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse default container update arguments: %v", err)
|
|
}
|
|
if !request.startLog {
|
|
t.Fatal("container startup logs are not enabled by default")
|
|
}
|
|
}
|
|
|
|
// TestParseUpdateArgsRejectsNativeJARAndContainerImageTogether 验证同时提供 --native-jar 与
|
|
// --container-image 时会被拒绝,并返回互斥错误提示。
|
|
func TestParseUpdateArgsRejectsNativeJARAndContainerImageTogether(t *testing.T) {
|
|
jarPath := filepath.Join(t.TempDir(), "glory-soft-yms.jar")
|
|
arguments := []string{
|
|
"--service", "backend",
|
|
"--native-jar", jarPath,
|
|
"--container-image", "harbor.ymswell.asia/ymswell/glory-ymswell:20260813-184902-a37bf50d-v1.1.8.1",
|
|
}
|
|
_, err := parseUpdateArgs(arguments, &bytes.Buffer{})
|
|
if err == nil || !strings.Contains(err.Error(), "exactly one") {
|
|
t.Fatalf("expected mutually exclusive backend inputs, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestParseUpdateArgsAcceptsQuite 验证 --quite 选项能被解析并保留到参数结果中。
|
|
func TestParseUpdateArgsAcceptsQuite(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "glory-soft-yms.jar")
|
|
if err := os.WriteFile(file, []byte("jar"), 0o600); err != nil {
|
|
t.Fatalf("write native backend JAR: %v", err)
|
|
}
|
|
request, err := parseUpdateArgs([]string{"--service", "backend", "--native-jar", file, "--quite"}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse quiet direct update arguments: %v", err)
|
|
}
|
|
if !request.quite {
|
|
t.Fatal("--quite was not retained")
|
|
}
|
|
}
|
|
|
|
// TestWriteUpdateProgress 验证进度事件的格式化输出:状态字段左对齐占 14 列后紧跟消息。
|
|
func TestWriteUpdateProgress(t *testing.T) {
|
|
var output bytes.Buffer
|
|
writeUpdateProgress(&output, daemonapi.Response{State: "STARTING", Message: "Starting backend"})
|
|
if output.String() != "STARTING Starting backend\n" {
|
|
t.Fatalf("unexpected update progress: %q", output.String())
|
|
}
|
|
}
|
|
|
|
// TestParseRestartArgs 验证 restart 子命令能接受 backend 服务与 --quite 选项。
|
|
func TestParseRestartArgs(t *testing.T) {
|
|
request, err := parseRestartArgs([]string{"--service", "backend", "--quite"}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse backend restart arguments: %v", err)
|
|
}
|
|
if request.service != serviceBackend || !request.quite {
|
|
t.Fatalf("unexpected backend restart arguments: %+v", request)
|
|
}
|
|
}
|
|
|
|
// TestParseRestartArgsRejectsUnsupportedInput 验证 restart 子命令对缺失服务、非 backend 服务
|
|
// 以及位置参数等非法输入均会拒绝。
|
|
func TestParseRestartArgsRejectsUnsupportedInput(t *testing.T) {
|
|
for name, arguments := range map[string][]string{
|
|
"missing service": nil,
|
|
"other service": {"--service", "frontend"},
|
|
"positional": {"--service", "backend", "extra"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := parseRestartArgs(arguments, &bytes.Buffer{}); err == nil {
|
|
t.Fatal("expected restart argument rejection")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseUpdateArgsRejectsIncompleteOrUnsupportedInput 验证 update 子命令对缺失服务、缺失文件、
|
|
// 非 backend 服务、位置参数以及多个互斥输入等非法组合均会拒绝。
|
|
func TestParseUpdateArgsRejectsIncompleteOrUnsupportedInput(t *testing.T) {
|
|
for name, arguments := range map[string][]string{
|
|
"missing service": {"-f", "/tmp/package.zip"},
|
|
"missing file": {"--service", "backend"},
|
|
"other service": {"--service", "frontend", "-f", "/tmp/package.zip"},
|
|
"positional": {"--service", "backend", "-f", "/tmp/package.zip", "extra"},
|
|
"both inputs": {"--service", "backend", "-f", "/tmp/package.zip", "--native-jar", "/tmp/backend.jar"},
|
|
"native and container": {"--service", "backend", "--native-jar", "/tmp/backend.jar", "--container-image", "repository:tag"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := parseUpdateArgs(arguments, &bytes.Buffer{}); err == nil {
|
|
t.Fatal("expected update argument rejection")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRunPrintsUsageWithoutCommand 验证不带任何子命令调用 run 时返回退出码 2 并向 stderr 输出用法。
|
|
func TestRunPrintsUsageWithoutCommand(t *testing.T) {
|
|
var stderr bytes.Buffer
|
|
if exitCode := run(context.Background(), nil, &bytes.Buffer{}, &stderr); exitCode != 2 {
|
|
t.Fatalf("unexpected exit code: %d", exitCode)
|
|
}
|
|
if stderr.Len() == 0 {
|
|
t.Fatal("missing usage output")
|
|
}
|
|
}
|
|
|
|
// TestParseDiagnosisArgsAcceptsService 验证 status/doctor/reconcile 子命令能接受 backend 服务,
|
|
// 且 reconcile 能接受 --apply。
|
|
func TestParseDiagnosisArgsAcceptsService(t *testing.T) {
|
|
for _, command := range []string{"status", "doctor", "reconcile"} {
|
|
request, err := parseDiagnosisArgs(command, []string{"--service", "backend"}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse %s arguments: %v", command, err)
|
|
}
|
|
if request.service != serviceBackend {
|
|
t.Fatalf("unexpected %s service: %+v", command, request)
|
|
}
|
|
}
|
|
request, err := parseDiagnosisArgs("reconcile", []string{"--service", "backend", "--apply"}, &bytes.Buffer{})
|
|
if err != nil {
|
|
t.Fatalf("parse reconcile apply arguments: %v", err)
|
|
}
|
|
if !request.apply {
|
|
t.Fatal("reconcile --apply was not retained")
|
|
}
|
|
}
|
|
|
|
// TestParseDiagnosisArgsRejectsUnsupportedInput 验证诊断子命令拒绝缺失服务、
|
|
// 非 backend 服务、位置参数以及非 reconcile 命令使用 --apply。
|
|
func TestParseDiagnosisArgsRejectsUnsupportedInput(t *testing.T) {
|
|
for name, test := range map[string]struct {
|
|
command string
|
|
arguments []string
|
|
}{
|
|
"missing service": {"status", nil},
|
|
"other service": {"doctor", []string{"--service", "frontend"}},
|
|
"positional": {"status", []string{"--service", "backend", "extra"}},
|
|
"apply on status": {"status", []string{"--service", "backend", "--apply"}},
|
|
"apply on doctor": {"doctor", []string{"--service", "backend", "--apply"}},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := parseDiagnosisArgs(test.command, test.arguments, &bytes.Buffer{}); err == nil {
|
|
t.Fatal("expected diagnosis argument rejection")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWriteDiagnosisFiltersByCommand 验证 writeDiagnosis 按子命令过滤诊断项:
|
|
// status 只展示漂移与可修复项,doctor 展示全部,reconcile 只展示可修复项。
|
|
func TestWriteDiagnosisFiltersByCommand(t *testing.T) {
|
|
diagnosis := daemonapi.Diagnosis{
|
|
Service: "backend",
|
|
Type: "container",
|
|
Healthy: true,
|
|
Items: []daemonapi.DiagnosisItem{
|
|
{Level: daemonapi.DiagnosisLevelOK, Code: "active", Message: "活动容器健康"},
|
|
{Level: daemonapi.DiagnosisLevelFixable, Code: "nginx", Message: "Nginx 指向错误", Action: "切流到 8080"},
|
|
},
|
|
}
|
|
|
|
var statusOut, doctorOut, reconcileOut bytes.Buffer
|
|
writeDiagnosis(&statusOut, "status", diagnosis)
|
|
writeDiagnosis(&doctorOut, "doctor", diagnosis)
|
|
writeDiagnosis(&reconcileOut, "reconcile", diagnosis)
|
|
|
|
if !strings.Contains(statusOut.String(), "Nginx 指向错误") || strings.Contains(statusOut.String(), "活动容器健康") {
|
|
t.Fatalf("status should hide ok items: %q", statusOut.String())
|
|
}
|
|
if !strings.Contains(doctorOut.String(), "活动容器健康") || !strings.Contains(doctorOut.String(), "Nginx 指向错误") {
|
|
t.Fatalf("doctor should show all items: %q", doctorOut.String())
|
|
}
|
|
if !strings.Contains(reconcileOut.String(), "Nginx 指向错误") || strings.Contains(reconcileOut.String(), "活动容器健康") {
|
|
t.Fatalf("reconcile should only show fixable items: %q", reconcileOut.String())
|
|
}
|
|
if !strings.Contains(statusOut.String(), "status=REPAIRABLE") {
|
|
t.Fatalf("status should distinguish fixable drift: %q", statusOut.String())
|
|
}
|
|
if !strings.Contains(reconcileOut.String(), "未执行") || !strings.Contains(reconcileOut.String(), "--apply") {
|
|
t.Fatalf("reconcile plan should explain that no fix was applied: %q", reconcileOut.String())
|
|
}
|
|
|
|
var appliedOut bytes.Buffer
|
|
diagnosis.RepairApplied = true
|
|
diagnosis.RepairTransactionID = "TX-1"
|
|
writeDiagnosis(&appliedOut, "reconcile", diagnosis)
|
|
if !strings.Contains(appliedOut.String(), "APPLIED") || !strings.Contains(appliedOut.String(), "TX-1") {
|
|
t.Fatalf("reconcile apply should show transaction: %q", appliedOut.String())
|
|
}
|
|
}
|