package httpapi import ( "context" "net/http" "net/http/httptest" "strings" "testing" "git.pcux.cn/xuans/Scert/backend/internal/certificate" "git.pcux.cn/xuans/Scert/backend/internal/deployment" "git.pcux.cn/xuans/Scert/backend/internal/domain" "git.pcux.cn/xuans/Scert/backend/internal/httpchallenge" "git.pcux.cn/xuans/Scert/backend/internal/repository" ) func TestHTTPChallengeProductionHandler(t *testing.T) { store := httpchallenge.NewStore() lease, err := store.NewLease(t.Context(), []string{"issued.example.com"}) if err != nil { t.Fatal(err) } defer lease.Close() const token = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" if err := lease.Present(t.Context(), "issued.example.com", token, token+"."+token); err != nil { t.Fatal(err) } server := New(Dependencies{Config: readyTestConfig(t.TempDir()), HTTPChallenges: store}) for _, tc := range []struct { host, path string status int }{ {"issued.example.com", token, 200}, {"other.example.com", token, 404}, {"issued.example.com", "unknown", 404}, {"issued.example.com", token + "/", 404}, } { w := httptest.NewRecorder() server.Handler().ServeHTTP(w, httptest.NewRequest("GET", "http://"+tc.host+httpchallenge.PathPrefix+tc.path, nil)) if w.Code != tc.status || w.Header().Get("Location") != "" || w.Header().Get("Cache-Control") != "no-store" { t.Fatalf("正式挑战路由错误:%d %s", w.Code, w.Body.String()) } } server.httpChallenges = nil w := httptest.NewRecorder() server.Handler().ServeHTTP(w, httptest.NewRequest("GET", "http://issued.example.com"+httpchallenge.PathPrefix+token, nil)) if w.Code != 404 { t.Fatal("缺失挑战存储时未拒绝") } } func TestHTTPDomainCreateAndUpdateBypassDNSOnly(t *testing.T) { cfg := readyTestConfig(t.TempDir()) cfg.DNS.Provider = "aliyun" repo := &stubDomainRepository{loaded: domain.Domain{ID: "domain-1", Name: "example.com", ChallengeType: domain.ChallengeHTTP01}} server := New(Dependencies{ Config: cfg, Repository: repo, ProjectDomains: projectDomainRepositoryStub{DomainRepository: repo}, ProjectResources: projectResourceReaderStub{domains: repo}, }) for _, tc := range []struct { method, path, body string status int }{ {"POST", "/api/v1/domains", `{"name":"example.com","certificate_type":"single","challenge_type":"http-01"}`, 201}, {"POST", "/api/v1/domains", `{"name":"example.com","certificate_type":"san","sans":["*.example.com"],"challenge_type":"http-01"}`, 400}, {"POST", "/api/v1/domains", `{"name":"example.com","certificate_type":"single","challenge_type":"unknown"}`, 400}, {"PUT", "/api/v1/domains/domain-1", `{"name":"example.com","certificate_type":"single"}`, 200}, } { w := httptest.NewRecorder() serveAdminHTTP(server, w, httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))) if w.Code != tc.status { t.Fatalf("域名接口状态 %d,预期 %d:%s", w.Code, tc.status, w.Body.String()) } } if repo.created.ChallengeType != domain.ChallengeHTTP01 || repo.updated.ChallengeType != domain.ChallengeHTTP01 { t.Fatal("HTTP 验证方式未保存或旧客户端覆盖了现有方式") } } func TestDomainCreateCanIncludeWildcardForMultiLevelName(t *testing.T) { item, err := newDomainFromRequest(domainRequest{ Name: "x.x.x.com", CertificateType: string(domain.CertificateSingle), IncludeWildcard: true, ChallengeType: stringPointer(domain.ChallengeDNS01), }) if err != nil { t.Fatalf("主域名同时申请泛域名失败:%v", err) } if item.CertificateType != domain.CertificateSAN { t.Fatalf("联合申请应使用 SAN 证书链路:%s", item.CertificateType) } if len(item.SANs) != 2 || item.SANs[0] != "x.x.x.com" || item.SANs[1] != "*.x.x.x.com" { t.Fatalf("联合申请的多级域名标识不正确:%v", item.SANs) } _, err = newDomainFromRequest(domainRequest{ Name: "x.x.x.com", CertificateType: string(domain.CertificateSingle), IncludeWildcard: true, ChallengeType: stringPointer(domain.ChallengeHTTP01), }) if err == nil { t.Fatal("主域名同时申请泛域名时不应允许 HTTP-01") } } func stringPointer(value string) *string { return &value } func TestHTTPTaskAuthorizationRetainsProjectPermissions(t *testing.T) { for _, denied := range []domain.ProjectPermission{domain.PermissionDNSCredentialUse, domain.PermissionCertificateManage, domain.PermissionDeploymentExecute} { t.Run(string(denied), func(t *testing.T) { repo := &stubDomainRepository{loaded: domain.Domain{ID: "domain-1", Name: "example.com", ChallengeType: domain.ChallengeHTTP01, ResourceScope: domain.ResourceScope{ProjectID: "project-a"}}} calls := 0 server := New(Dependencies{ Config: readyTestConfig(t.TempDir()), Repository: repo, ProjectResources: projectResourceReaderStub{domains: repo}, ProjectMembers: permissionProjectMemberRepository{allow: func(permission domain.ProjectPermission) error { if permission == denied { return repository.ErrProjectAccessDenied } return nil }}, IssueCertificate: func(context.Context, string) (certificate.IssueResult, deployment.Report, error) { calls++ return certificate.IssueResult{}, deployment.Report{}, nil }, }) w := httptest.NewRecorder() serveAdminHTTP(server, w, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil)) want := denied == domain.PermissionDNSCredentialUse if (calls == 1) != want { t.Fatalf("HTTP 签发项目授权错误:%d %s", w.Code, w.Body.String()) } err := server.authorizeCertificateTaskExecution(t.Context(), repository.Task{DomainID: "domain-1", HistoryScope: repository.HistoryScope{ProjectID: "project-a", ScopeType: repository.ScopeProject}, OperatorID: "actor", TaskType: "certificate_issue"}, repository.TaskAttempt{}) if (err == nil) != want { t.Fatal("执行前授权与验证方式不匹配:", err) } }) } }