145 lines
5.8 KiB
Go
145 lines
5.8 KiB
Go
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)
|
||
}
|
||
})
|
||
}
|
||
}
|