package deploymentconfig import ( "os" "path/filepath" "strings" "testing" ) // validNativeConfig 一份满足原生后端部署契约的示例 TOML 配置,供各测试用例共享。 const validNativeConfig = `[daemon] environment = "dev" [backend] type = "native" release_dir = "/home/yms/lib/releases" active_jar = "/home/yms/lib/glory-soft-yms.jar" systemctl_path = "/bin/systemctl" [backend.slot.8080] unit = "yms-backend@8080.service" jar = "/home/yms/lib/glory-soft-yms-8080.jar" health_endpoint = "http://127.0.0.1:8080/yms/actuator/health" [backend.slot.8081] unit = "yms-backend@8081.service" jar = "/home/yms/lib/glory-soft-yms-8081.jar" health_endpoint = "http://127.0.0.1:8081/yms/actuator/health" [nodeSsr] type = "container" [nodeSsr.slot.18910] container_name = "node-ssr-18910" health_endpoint = "http://127.0.0.1:18910/health" [nodeSsr.slot.28910] container_name = "node-ssr-28910" health_endpoint = "http://127.0.0.1:28910/health" ` // validContainerConfig 一份满足容器后端部署契约的示例 TOML 配置,供各测试用例共享。 const validContainerConfig = `[daemon] environment = "dev" [backend] type = "container" systemctl_path = "/bin/systemctl" [backend.slot.8080] container_name = "backend-8080" health_endpoint = "http://127.0.0.1:8080/yms/actuator/health" [backend.slot.8081] container_name = "backend-8081" health_endpoint = "http://127.0.0.1:8081/yms/actuator/health" [nodeSsr] type = "container" [nodeSsr.slot.18910] container_name = "node-ssr-18910" health_endpoint = "http://127.0.0.1:18910/health" [nodeSsr.slot.28910] container_name = "node-ssr-28910" health_endpoint = "http://127.0.0.1:28910/health" ` // TestLoadValidNativeConfiguration 验证合法原生配置能被完整加载, // 且解析出的 daemon、backend 与 8080/8081 槽位取值都与契约常量精确一致。 func TestLoadValidNativeConfiguration(t *testing.T) { path := writeConfig(t, validNativeConfig) config, err := Load(path) if err != nil { t.Fatalf("load native deployment configuration: %v", err) } if config.Daemon.Environment != EnvironmentDev { t.Fatalf("unexpected daemon configuration: %+v", config.Daemon) } if config.Backend.Type != BackendTypeNative || config.Backend.SystemctlPath != "/bin/systemctl" { t.Fatalf("unexpected backend configuration: %+v", config.Backend) } slot8080, err := config.Backend.SlotForPort(BackendPort8080) if err != nil { t.Fatalf("read 8080 slot: %v", err) } if slot8080.Unit != nativeUnit8080 || slot8080.JAR != nativeSlotJAR8080 || slot8080.HealthEndpoint != nativeHealthURL8080 { t.Fatalf("unexpected 8080 slot: %+v", slot8080) } slot8081, err := config.Backend.SlotForPort(BackendPort8081) if err != nil { t.Fatalf("read 8081 slot: %v", err) } if slot8081.Unit != nativeUnit8081 || slot8081.JAR != nativeSlotJAR8081 || slot8081.HealthEndpoint != nativeHealthURL8081 { t.Fatalf("unexpected 8081 slot: %+v", slot8081) } } // TestLoadValidContainerConfiguration 验证合法容器配置能被加载, // 且两个槽位的 container_name 取值与契约精确一致。 func TestLoadValidContainerConfiguration(t *testing.T) { config, err := Load(writeConfig(t, validContainerConfig)) if err != nil { t.Fatalf("load container deployment configuration: %v", err) } if config.Backend.Type != BackendTypeContainer { t.Fatalf("unexpected backend type: %q", config.Backend.Type) } if config.Backend.Slot.Port8080.ContainerName != "backend-8080" || config.Backend.Slot.Port8081.ContainerName != "backend-8081" { t.Fatalf("unexpected container slots: %+v", config.Backend.Slot) } } // TestLoadAcceptsStopGraceSeconds 验证 backend.stop_grace_seconds 配置为合法正整数时 // 能被接受,并解析到 Backend.StopGraceSeconds。 func TestLoadAcceptsStopGraceSeconds(t *testing.T) { content := strings.Replace(validContainerConfig, "systemctl_path = \"/bin/systemctl\"", "systemctl_path = \"/bin/systemctl\"\nstop_grace_seconds = 45", 1) config, err := Load(writeConfig(t, content)) if err != nil { t.Fatalf("load configuration with stop_grace_seconds: %v", err) } if config.Backend.StopGraceSeconds == nil || *config.Backend.StopGraceSeconds != 45 { t.Fatalf("unexpected stop_grace_seconds: %+v", config.Backend.StopGraceSeconds) } } // TestLoadRejectsNonPositiveStopGraceSeconds 验证 backend.stop_grace_seconds 配置为非正整数时被拒绝。 func TestLoadRejectsNonPositiveStopGraceSeconds(t *testing.T) { content := strings.Replace(validContainerConfig, "systemctl_path = \"/bin/systemctl\"", "systemctl_path = \"/bin/systemctl\"\nstop_grace_seconds = 0", 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "backend.stop_grace_seconds") { t.Fatalf("expected stop_grace_seconds rejection, got %v", err) } } // TestLoadRejectsChangedContainerName 验证容器槽位的 container_name 被篡改时会被拒绝, // 且错误信息精确指向 backend.slot.8081.container_name 字段。 func TestLoadRejectsChangedContainerName(t *testing.T) { content := strings.Replace(validContainerConfig, `container_name = "backend-8081"`, `container_name = "backend"`, 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "backend.slot.8081.container_name") { t.Fatalf("expected exact container name rejection, got %v", err) } } // TestPackagedNativeConfigurationMatchesContract 验证随发行包打包的原生部署配置 // 与代码内置契约一致,确保打包产物不会被意外改坏。 func TestPackagedNativeConfigurationMatchesContract(t *testing.T) { path, err := filepath.Abs(filepath.Join("..", "..", "packaging", "etc", "yms-daemon", "yms-daemon.toml")) if err != nil { t.Fatalf("resolve packaged native deployment configuration: %v", err) } if _, err := Load(path); err != nil { t.Fatalf("load packaged native deployment configuration: %v", err) } } // TestLoadRejectsUnknownField 验证配置中出现的未知字段(大小写不一致)会被拒绝, // 且错误信息精确包含该字段名 SystemctlPath。 func TestLoadRejectsUnknownField(t *testing.T) { content := strings.Replace(validNativeConfig, "systemctl_path = \"/bin/systemctl\"", "systemctl_path = \"/bin/systemctl\"\nSystemctlPath = \"/usr/bin/systemctl\"", 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "SystemctlPath") { t.Fatalf("expected exact unknown field rejection, got %v", err) } } // TestLoadAcceptsProductionEnvironment 验证 environment 取值为 prod 时能被接受, // 并正确解析到 EnvironmentProd。 func TestLoadAcceptsProductionEnvironment(t *testing.T) { content := strings.Replace(validNativeConfig, `environment = "dev"`, `environment = "prod"`, 1) config, err := Load(writeConfig(t, content)) if err != nil { t.Fatalf("load production environment: %v", err) } if config.Daemon.Environment != EnvironmentProd { t.Fatalf("unexpected daemon environment: %q", config.Daemon.Environment) } } // TestLoadRejectsMissingDaemonTable 验证缺失 daemon 顶层表时会被拒绝, // 且错误信息提示 daemon 表是必需的。 func TestLoadRejectsMissingDaemonTable(t *testing.T) { content := strings.Replace(validNativeConfig, "[daemon]\nenvironment = \"dev\"\n\n", "", 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "daemon table is required") { t.Fatalf("expected missing daemon table rejection, got %v", err) } } // TestLoadRejectsChangedEnvironment 验证 environment 被篡改为非法取值时会被拒绝, // 且错误信息精确指向 daemon.environment 字段。 func TestLoadRejectsChangedEnvironment(t *testing.T) { content := strings.Replace(validNativeConfig, `environment = "dev"`, `environment = "development"`, 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "daemon.environment") { t.Fatalf("expected exact daemon environment rejection, got %v", err) } } // TestLoadRejectsUnknownDaemonField 验证 daemon 表中出现的未知字段(大小写不一致) // 会被拒绝,且错误信息精确包含该字段名 daemon.Environment。 func TestLoadRejectsUnknownDaemonField(t *testing.T) { content := strings.Replace(validNativeConfig, `environment = "dev"`, "environment = \"dev\"\nEnvironment = \"dev\"", 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "daemon.Environment") { t.Fatalf("expected exact daemon field rejection, got %v", err) } } // TestLoadRejectsWrongTableCase 验证表名大小写不一致([Backend])时会被拒绝, // 且错误信息精确包含该表名 Backend。 func TestLoadRejectsWrongTableCase(t *testing.T) { content := strings.Replace(validNativeConfig, "[backend]", "[Backend]", 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "Backend") { t.Fatalf("expected exact table case rejection, got %v", err) } } // TestLoadRejectsMissingNativeField 验证原生配置缺失 active_jar 字段时会被拒绝, // 且错误信息精确指向 backend.active_jar 字段。 func TestLoadRejectsMissingNativeField(t *testing.T) { content := strings.Replace(validNativeConfig, "active_jar = \"/home/yms/lib/glory-soft-yms.jar\"\n", "", 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "backend.active_jar") { t.Fatalf("expected missing active JAR rejection, got %v", err) } } // TestLoadRejectsUnknownSlot 验证配置中出现未受支持的槽位(9090)时会被拒绝, // 且错误信息精确包含该端口号 9090。 func TestLoadRejectsUnknownSlot(t *testing.T) { content := validNativeConfig + ` [backend.slot.9090] unit = "yms-backend@9090.service" jar = "/home/yms/lib/glory-soft-yms-9090.jar" health_endpoint = "http://127.0.0.1:9090/yms/actuator/health" ` _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "9090") { t.Fatalf("expected unknown slot rejection, got %v", err) } } // TestLoadRejectsChangedSlotEndpoint 验证槽位健康端点被篡改时会被拒绝, // 且错误信息精确指向 backend.slot.8080.health_endpoint 字段。 func TestLoadRejectsChangedSlotEndpoint(t *testing.T) { content := strings.Replace(validNativeConfig, nativeHealthURL8080, nativeHealthURL8081, 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "backend.slot.8080.health_endpoint") { t.Fatalf("expected changed health endpoint rejection, got %v", err) } } // TestLoadAcceptsExplicitUsrBinSystemctlPath 验证显式指定的 /usr/bin/systemctl 路径 // 能被接受并原样解析,说明 systemctl_path 只要求绝对路径而不绑定单一取值。 func TestLoadAcceptsExplicitUsrBinSystemctlPath(t *testing.T) { content := strings.Replace(validNativeConfig, "/bin/systemctl", "/usr/bin/systemctl", 1) config, err := Load(writeConfig(t, content)) if err != nil { t.Fatalf("load explicit /usr/bin/systemctl path: %v", err) } if config.Backend.SystemctlPath != "/usr/bin/systemctl" { t.Fatalf("unexpected systemctl path: %q", config.Backend.SystemctlPath) } } // TestLoadRejectsChangedBackendTypeCase 验证 backend.type 大小写不一致(Native)时会被拒绝, // 且错误信息精确指向 backend.type 字段。 func TestLoadRejectsChangedBackendTypeCase(t *testing.T) { content := strings.Replace(validNativeConfig, `type = "native"`, `type = "Native"`, 1) _, err := Load(writeConfig(t, content)) if err == nil || !strings.Contains(err.Error(), "backend.type") { t.Fatalf("expected exact backend type rejection, got %v", err) } } // TestLoadRejectsDuplicateField 验证 TOML 中重复出现的字段会被拒绝, // 确保配置没有歧义。 func TestLoadRejectsDuplicateField(t *testing.T) { content := strings.Replace(validNativeConfig, "type = \"native\"", "type = \"native\"\ntype = \"native\"", 1) _, err := Load(writeConfig(t, content)) if err == nil { t.Fatal("expected duplicate field rejection") } } // TestLoadRequiresAbsoluteConfigurationPath 验证 Load 要求传入绝对路径, // 相对路径会被拒绝且错误信息提示 absolute path。 func TestLoadRequiresAbsoluteConfigurationPath(t *testing.T) { _, err := Load("yms-daemon.toml") if err == nil || !strings.Contains(err.Error(), "absolute path") { t.Fatalf("expected absolute configuration path rejection, got %v", err) } } // TestSlotForPortRejectsUnsupportedPort 验证 SlotForPort 对未受支持端口返回错误。 func TestSlotForPortRejectsUnsupportedPort(t *testing.T) { config, err := Load(writeConfig(t, validNativeConfig)) if err != nil { t.Fatalf("load native deployment configuration: %v", err) } if _, err := config.Backend.SlotForPort(9090); err == nil { t.Fatal("expected unsupported port rejection") } } // writeConfig 将 content 写入临时目录下的 yms-daemon.toml 并返回其绝对路径, // 供 Load 测试使用。写入失败会直接终止测试。 func writeConfig(t *testing.T, content string) string { t.Helper() path := filepath.Join(t.TempDir(), "yms-daemon.toml") if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("write deployment configuration: %v", err) } return path }