package bootstrap import ( "context" "errors" "os" "reflect" "strings" "testing" "git.pcux.cn/xuans/Scert/backend/internal/config" ) func completionNotificationInput() *NotificationInput { return &NotificationInput{ HTTPEnabled: true, Endpoint: "https://mail.example.test", HTTPAuthMode: "session", HTTPCredentials: map[string]string{"account": "mail-owner", "password": "private-http-password"}, SMTPEnabled: true, SMTPHost: "smtp.example.test", SMTPUsername: "notify:smtp-owner@example.test", SMTPPassword: "private-smtp-password", IncludeManagementURL: true, IncludeCertificateSerial: true, Recipients: []string{"notify@example.test"}, TimeoutSecs: 15, RetryCount: 0, RetryIntervalsSecs: []int{}, } } func TestCompleterPreservesExistingNotificationWhenOmitted(t *testing.T) { fixture := newCompletionFixture(t, "admin.example.test") fixture.base.Notification.HTTPEnabled = true fixture.base.Notification.Endpoint = "https://mail.example.test" fixture.base.Notification.HTTPAuthMode = "bearer" fixture.base.Notification.HTTPCredentialRef = "secret://notification/existing" fixture.base.Notification.SenderEmail = "sender@example.test" fixture.base.Notification.Recipients = []string{"notify@example.test"} fixture.base.Notification.HTTPHeaders = map[string]string{"X-Client": "Scert"} fixture.base.Notification.IncludeDeploymentDetails = true if _, err := fixture.completer(nil).Complete(t.Context(), fixture.request("admin.example.test")); err != nil { t.Fatal(err) } stored, err := config.Load(fixture.configPath) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(stored.Notification, fixture.base.Notification) { t.Fatalf("省略通知表单时原有通知设置发生丢失:%+v", stored.Notification) } } func TestCompleterStoresNotificationCredentialsWithoutPlaintext(t *testing.T) { fixture := newCompletionFixture(t, "admin.example.test") request := fixture.request("admin.example.test") request.Notification = completionNotificationInput() if _, err := fixture.completer(nil).Complete(t.Context(), request); err != nil { t.Fatal(err) } stored, err := config.Load(fixture.configPath) if err != nil { t.Fatal(err) } if !stored.Notification.HTTPEnabled || !stored.Notification.SMTPEnabled || stored.Notification.RetryCount != 0 || len(stored.Notification.RetryIntervalsSecs) != 0 || !stored.Notification.IncludeCertificateSerial { t.Fatalf("通知选项未完整保存:%+v", stored.Notification) } httpCredentials, err := fixture.store.Get(t.Context(), stored.Notification.HTTPCredentialRef) if stored.Notification.SenderEmail != "notification+system@example.test" || stored.Notification.SenderName != "系统通知" { t.Fatal("初始化没有保存 Smail 系统通知发件人") } if err != nil || httpCredentials["password"] != "private-http-password" { t.Fatalf("HTTP 通知凭据未进入敏感存储:%v", err) } smtpCredentials, err := fixture.store.Get(t.Context(), stored.Notification.SMTPCredentialRef) if err != nil || smtpCredentials["password"] != "private-smtp-password" { t.Fatalf("SMTP 通知凭据未进入敏感存储:%v", err) } data, err := os.ReadFile(fixture.configPath) if err != nil { t.Fatal(err) } for _, secret := range []string{"private-http-password", "private-smtp-password"} { if strings.Contains(string(data), secret) { t.Fatal("正式配置包含通知明文密码") } } } func TestCompleterReclaimsOnlyNewNotificationCredentialsOnFailure(t *testing.T) { fixture := newCompletionFixture(t, "admin.example.test") existing := "secret://notification/existing" if err := fixture.store.Put(t.Context(), existing, map[string]string{"token": "existing-token"}); err != nil { t.Fatal(err) } request := fixture.request("admin.example.test") request.Notification = completionNotificationInput() _, err := fixture.completer(func(dependencies *CompleteDependencies) { dependencies.SaveConfig = func(config.Config, string) error { return errors.New("save failed") } }).Complete(t.Context(), request) if err == nil { t.Fatal("保存失败未返回错误") } fixture.store.mu.Lock() defer fixture.store.mu.Unlock() if len(fixture.store.values) != 1 || fixture.store.values[existing]["token"] != "existing-token" { t.Fatalf("回滚必须保留已有凭据并回收新凭据:剩余=%d", len(fixture.store.values)) } } type failingNotificationStore struct { *completionMemorySecrets puts int } func (store *failingNotificationStore) Put(ctx context.Context, reference string, values map[string]string) error { store.puts++ if err := store.completionMemorySecrets.Put(ctx, reference, values); err != nil { return err } if store.puts == 2 { return errors.New("partial write failure") } return nil } func TestNotificationPartialWriteFailureReclaimsAllCreatedReferences(t *testing.T) { fixture := newCompletionFixture(t, "admin.example.test") store := &failingNotificationStore{completionMemorySecrets: fixture.store} request := fixture.request("admin.example.test") request.Notification = completionNotificationInput() _, err := fixture.completer(func(dependencies *CompleteDependencies) { dependencies.Credentials = store }).Complete(t.Context(), request) if err == nil || store.puts != 2 || fixture.issuer.calls != 0 { t.Fatalf("部分写入失败未中止初始化:%v", err) } fixture.store.mu.Lock() defer fixture.store.mu.Unlock() if len(fixture.store.values) != 0 { t.Fatal("部分写入失败遗留通知或 DNS 临时凭据") } } func TestNotificationAuthenticationModes(t *testing.T) { for _, item := range []struct { mode string input map[string]string key, value string }{ {"bearer", map[string]string{"access_token": "test-token"}, "token", "test-token"}, {"hmac_sha256", map[string]string{"hmac_secret": "test-key"}, "secret", "test-key"}, {"session", map[string]string{"username": "owner", "password": "test-password"}, "account", "owner"}, } { t.Run(item.mode, func(t *testing.T) { values, err := notificationCredentials(item.mode, item.input) if err != nil || values[item.key] != item.value { t.Fatalf("通知凭据兼容字段转换不正确:%v", err) } if _, err := notificationCredentials(item.mode, nil); err == nil { t.Fatal("缺少通知认证凭据未拒绝") } }) } } func TestNotificationRejectsInvalidSettingsBeforeWritingSecrets(t *testing.T) { for _, edit := range []func(*NotificationInput){ func(input *NotificationInput) { input.Endpoint = "" }, func(input *NotificationInput) { input.RetryCount = -1 }, func(input *NotificationInput) { input.SMTPPassword = "" }, func(input *NotificationInput) { input.SMTPMode = "plain" }, func(input *NotificationInput) { input.HTTPCredentials = nil }, } { fixture := newCompletionFixture(t, "admin.example.test") input := completionNotificationInput() edit(input) _, cleanup, err := fixture.completer(nil).prepareNotification(t.Context(), fixture.base.Notification, input) if err == nil || cleanup != nil { t.Fatal("无效通知配置不得写入敏感存储") } } } func TestManagementURLHandlesDirectAndProxyEntrypoints(t *testing.T) { for _, item := range []struct { hostname, ip, mode string port int proxyPort int want string }{ {"admin.example.test", "", "direct", 443, 8004, "https://admin.example.test/"}, {"admin.example.test", "", "direct", 8443, 8004, "https://admin.example.test:8443/"}, {"admin.example.test", "", "proxy", 8080, 0, "https://admin.example.test/"}, {"admin.example.test", "", "proxy", 9004, 8004, "https://admin.example.test:8004/"}, {"other.example.test", "", "proxy", 9004, 443, "https://other.example.test/"}, {"", "203.0.113.70", "direct", 8443, 0, "https://203.0.113.70:8443/"}, {"", "2001:db8::70", "direct", 443, 0, "https://[2001:db8::70]/"}, {"", "2001:db8::70", "direct", 8443, 0, "https://[2001:db8::70]:8443/"}, {"", "2001:db8::70", "proxy", 8080, 0, "https://[2001:db8::70]/"}, {"", "2001:db8::70", "proxy", 9004, 8004, "https://[2001:db8::70]:8004/"}, } { candidate := config.Config{ Web: config.WebConfig{Hostname: item.hostname, PublicIP: item.ip}, Server: config.ServerConfig{Port: item.port, HTTPSMode: item.mode, ProxyHTTPSPort: item.proxyPort}, } if actual := managementURL(candidate); actual != item.want { t.Fatalf("管理入口不正确:实际=%s,预期=%s", actual, item.want) } if actual := candidate.ResetURL(); actual != item.want+"reset" { t.Fatalf("重置入口与管理入口不一致:%s", actual) } } }