Scert/backend/internal/app/app_test.go

145 lines
5.7 KiB
Go

package app
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/config"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"git.pcux.cn/xuans/Scert/backend/internal/repository"
"git.pcux.cn/xuans/Scert/backend/internal/setup"
)
func TestNewWiresApplicationServices(t *testing.T) {
cfg := readyApplicationConfig(t)
application, err := NewWithConfigPath(context.Background(), cfg, filepath.Join(cfg.Paths.Config, "scert.json"))
if err != nil {
t.Fatalf("初始化应用失败:%v", err)
}
defer application.Close()
if application.Repository == nil || application.Servers == nil || application.Tasks == nil {
t.Fatal("数据库仓储未完整注入应用实例")
}
if application.Deployment == nil || application.Certificates == nil || application.Workflow == nil || application.Renewal == nil {
t.Fatal("证书申请、部署或续期服务未完整注入应用实例")
}
if application.SecretStore == nil || application.SSHKeys == nil || application.DNSVerification == nil || application.HTTP == nil {
t.Fatal("敏感信息、SSH、DNS 或 HTTP 服务未完整注入应用实例")
}
}
func TestNewWithConfigPathRecoversInterruptedTasks(t *testing.T) {
cfg := readyApplicationConfig(t)
configPath := filepath.Join(cfg.Paths.Config, "scert.json")
first, err := NewWithConfigPath(context.Background(), cfg, configPath)
if err != nil {
t.Fatalf("首次初始化应用失败:%v", err)
}
item, err := domain.NewDomain("restart.example.com", domain.CertificateSingle, nil)
if err != nil {
_ = first.Close()
t.Fatalf("创建测试域名失败:%v", err)
}
if err := first.Repository.Create(context.Background(), item); err != nil {
_ = first.Close()
t.Fatalf("保存测试域名失败:%v", err)
}
now := time.Date(2026, 8, 30, 12, 0, 0, 0, time.UTC)
tasks := []repository.Task{
{ID: "restart-queued", DomainID: item.ID, TaskType: "certificate_issue", Status: "queued", Stage: "queued", Message: "等待执行", CreatedAt: now, UpdatedAt: now},
{ID: "restart-running", DomainID: item.ID, TaskType: "certificate_deploy", Status: "running", Stage: "deploy", Message: "正在部署", StartedAt: &now, CreatedAt: now, UpdatedAt: now},
{ID: "restart-success", DomainID: item.ID, TaskType: "certificate_validate", Status: "success", Stage: "validate", Message: "已完成", FinishedAt: &now, CreatedAt: now, UpdatedAt: now},
}
for _, task := range tasks {
if err := first.Tasks.CreateTask(context.Background(), task); err != nil {
_ = first.Close()
t.Fatalf("保存测试任务 %s 失败:%v", task.ID, err)
}
}
attempt := repository.TaskAttempt{
ID: "restart-attempt", TaskID: "restart-running", AttemptNumber: 1, Stage: "deploy", Status: "running", Message: "正在部署", StartedAt: &now, CreatedAt: now,
}
if err := first.Tasks.CreateAttempt(context.Background(), attempt); err != nil {
_ = first.Close()
t.Fatalf("保存测试任务尝试失败:%v", err)
}
if err := first.Close(); err != nil {
t.Fatalf("关闭首次应用失败:%v", err)
}
second, err := NewWithConfigPath(context.Background(), cfg, configPath)
if err != nil {
t.Fatalf("重启后初始化应用失败:%v", err)
}
defer second.Close()
for _, expectedID := range []string{"restart-queued", "restart-running"} {
task, getErr := second.Tasks.GetTask(context.Background(), expectedID)
if getErr != nil {
t.Fatalf("读取恢复任务 %s 失败:%v", expectedID, getErr)
}
if task.Status != "failed" || task.Stage != "service_restart" || task.FinishedAt == nil {
t.Fatalf("任务 %s 未按服务重启恢复:%+v", expectedID, task)
}
if task.Message != "服务重启导致任务中断,请重新执行" {
t.Fatalf("任务 %s 中断说明不正确:%s", expectedID, task.Message)
}
}
recoveredAttempt, err := second.Tasks.ListAttempts(context.Background(), "restart-running")
if err != nil {
t.Fatalf("读取恢复任务尝试失败:%v", err)
}
if len(recoveredAttempt) != 1 || recoveredAttempt[0].Status != "failed" || recoveredAttempt[0].FinishedAt == nil {
t.Fatalf("任务尝试未按服务重启恢复:%+v", recoveredAttempt)
}
completed, err := second.Tasks.GetTask(context.Background(), "restart-success")
if err != nil {
t.Fatalf("读取已完成任务失败:%v", err)
}
if completed.Status != "success" || completed.Stage != "validate" || completed.Message != "已完成" {
t.Fatalf("已完成任务不应被重启恢复逻辑修改:%+v", completed)
}
}
func readyApplicationConfig(t *testing.T) config.Config {
t.Helper()
root := t.TempDir()
cfg := config.Default(root)
cfg.Paths = config.PathsConfig{
Install: root, Config: filepath.Join(root, "conf"), Data: filepath.Join(root, "data"),
Certificate: filepath.Join(root, "ssl"), Backup: filepath.Join(root, "backups"), Log: filepath.Join(root, "logs"),
}
cfg.Database.DSN = filepath.Join(cfg.Paths.Data, "scert.db")
if err := setup.Apply(&cfg, setup.Input{
AdminUsername: "admin", AdminEmail: "admin@example.test", AdminPassword: "Test1234!",
ServerPort: 8080, WebHostname: "scert.example.test", WebEnabled: true,
DNSProvider: "aliyun", DNSCredentialRef: "secret://dns/test", DNSVerified: true,
CertificateRoot: cfg.Paths.Certificate, HTTPSMode: "direct",
}); err != nil {
t.Fatal(err)
}
cfg.SetupReady = true
cfg.Web.InitReady = true
return cfg
}
func TestUninitializedApplicationHasNoSideEffects(t *testing.T) {
root := t.TempDir()
cfg := config.Default(root)
cfg.Paths.Install = filepath.Join(root, "must-not-exist")
if _, err := NewWithConfigPath(context.Background(), cfg, filepath.Join(root, "config.json")); err == nil {
t.Fatal("未初始化配置未被拒绝")
}
if _, err := os.Stat(cfg.Paths.Install); !os.IsNotExist(err) {
t.Fatalf("拒绝启动前产生了文件副作用:%v", err)
}
}